Added JavaScript tests in; Refactored some stuff, removed some stuff. Lots börken.

This commit is contained in:
Raoul Snyman 2017-10-04 00:44:08 -07:00
parent f6a91839fc
commit 640ebf8c3c
26 changed files with 1373 additions and 1129 deletions

View File

@ -44,3 +44,4 @@ coverage
tags
output
htmlcov
node_modules

77
karma.conf.js Normal file
View File

@ -0,0 +1,77 @@
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: "",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine"],
// list of files / patterns to load in the browser
files: [
"tests/js/polyfill.js",
"tests/js/fake_webchannel.js",
"openlp/core/display/html/reveal.js",
"openlp/core/display/html/display.js",
"tests/js/test_*.js"
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
"display.js": ["coverage"]
},
// test results reporter to use
// possible values: "dots", "progress"
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "coverage"],
// configure the coverateReporter
coverageReporter: {
type : "html",
dir : "htmlcov/"
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_DEBUG,
// loggers
/* loggers: [
{"type": "file", "filename": "karma.log"}
],*/
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["PhantomJS"],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client: {
captureConsole: true
}
})
}

View File

@ -22,5 +22,3 @@
"""
The Display module.
"""
from .canvas import MainCanvas, Canvas, DisplayControllerType
from .renderer import Renderer

View File

@ -0,0 +1,356 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.display.render` module contains functions for rendering.
"""
import html
import logging
import math
import re
from openlp.core.lib.formattingtags import FormattingTags
log = logging.getLogger(__name__)
SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?')
CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>'
CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'
WHITESPACE_TEMPLATE = '<span class="ws">{whitespaces}</span>'
def remove_tags(text, can_remove_chords=False):
"""
Remove Tags from text for display
:param text: Text to be cleaned
:param can_remove_chords: Can we remove the chords too?
"""
text = text.replace('<br>', '\n')
text = text.replace('{br}', '\n')
text = text.replace('&nbsp;', ' ')
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], '')
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if can_remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
return text
def has_valid_tags(text):
"""
The :func:`~openlp.core.display.render.has_valid_tags` function validates the tags within ``text``.
:param str text: The string with formatting tags in it.
:returns bool: Returns True if tags are valid, False if there are parsing problems.
"""
return True
def render_chords_in_line(match):
"""
Render the chords in the line and align them using whitespaces.
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
:param str match: The line which contains chords
:returns str: The line with rendered html-chords
"""
whitespaces = ''
chord_length = 0
tail_length = 0
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
# The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
tail = match.group(2)
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
remainder = match.group(3)
# Line end if found, else None
end = match.group(4)
# Based on char width calculate width of chord
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
# Based on char width calculate width of tail
for tail_char in tail:
if tail_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# Based on char width calculate width of remainder
for remainder_char in remainder:
if remainder_char not in SLIM_CHARS:
tail_length += 2
else:
tail_length += 1
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
if chord_length >= tail_length and end is None:
# Decide if the padding should be "_" for drawing out words or spaces
if tail:
if not remainder:
for c in range(math.ceil((chord_length - tail_length) / 2) + 2):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chord_length - tail_length) / 2)):
whitespaces += '_'
else:
for c in range(chord_length - tail_length + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chord_length):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + ws_right
whitespaces = WHITESPACE_TEMPLATE.format(whitespaces=whitespaces)
return CHORD_LINE_TEMPLATE.format(chord=html.escape(chord), tail=html.escape(tail), whitespace=whitespaces,
remainder=html.escape(remainder))
def render_chords(text):
"""
Render ChordPro tags
:param str text: The text containing the chords
:returns str: The text containing the rendered chords
"""
text_lines = text.split('{br}')
rendered_lines = []
chords_on_prev_line = False
for line in text_lines:
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
if '[' in line and ']' in line:
if chords_on_prev_line:
chord_template = CHORD_TEMPLATE
else:
chord_template = FIRST_CHORD_TEMPLATE
chords_on_prev_line = True
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
new_line = chord_template.format(chord=CHORD_LINE_MATCH.sub(render_chords_in_line, line))
rendered_lines.append(new_line)
else:
chords_on_prev_line = False
rendered_lines.append(html.escape(line))
return '{br}'.join(rendered_lines)
def compare_chord_lyric_width(chord, lyric):
"""
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
:param chord:
:param lyric:
:return:
"""
chord_length = 0
if chord == '&nbsp;':
return 0
chord = re.sub(r'\{.*?\}', r'', chord)
lyric = re.sub(r'\{.*?\}', r'', lyric)
for chord_char in chord:
if chord_char not in SLIM_CHARS:
chord_length += 2
else:
chord_length += 1
lyriclen = 0
for lyric_char in lyric:
if lyric_char not in SLIM_CHARS:
lyriclen += 2
else:
lyriclen += 1
if chord_length > lyriclen:
return chord_length - lyriclen
else:
return 0
def find_formatting_tags(text, active_formatting_tags):
"""
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
:param text:
:param active_formatting_tags:
:return:
"""
if not re.search(r'\{.*?\}', text):
return active_formatting_tags
word_iterator = iter(text)
# Loop through lyrics to find any formatting tags
for char in word_iterator:
if char == '{':
tag = ''
char = next(word_iterator)
start_tag = True
if char == '/':
start_tag = False
char = next(word_iterator)
while char != '}':
tag += char
char = next(word_iterator)
# See if the found tag has an end tag
for formatting_tag in FormattingTags.get_html_tags():
if formatting_tag['start tag'] == '{' + tag + '}':
if formatting_tag['end tag']:
if start_tag:
# prepend the new tag to the list of active formatting tags
active_formatting_tags[:0] = [tag]
else:
# remove the tag from the list
active_formatting_tags.remove(tag)
# Break out of the loop matching the found tag against the tag list.
break
return active_formatting_tags
def render_chords_for_printing(text, line_split):
"""
Render ChordPro tags for printing
:param str text: The text containing the chords to be rendered.
:param str line_split: The character(s) used to split lines
:returns str: The rendered chords
"""
if not re.search(r'\[.*?\]', text):
return text
text_lines = text.split(line_split)
rendered_text_lines = []
for line in text_lines:
# If a ChordPro is detected in the line, build html tables.
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
active_formatting_tags = []
if re.search(r'\[.*?\]', line):
words = line.split(' ')
in_chord = False
for word in words:
chords = []
lyrics = []
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
# If the word contains a chord, we need to handle it.
if re.search(r'\[.*?\]', word):
chord = ''
lyric = ''
# Loop over each character of the word
for char in word:
if char == '[':
in_chord = True
if lyric != '':
if chord == '':
chord = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
chord = ''
lyric = ''
elif char == ']' and in_chord:
in_chord = False
elif in_chord:
chord += char
else:
lyric += char
if lyric != '' or chord != '':
if chord == '':
chord = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
new_chord_line = '<tr class="chordrow">'
new_lyric_line = '</tr><tr>'
for i in range(len(lyrics)):
spacer = compare_chord_lyric_width(chords[i], lyrics[i])
# Handle formatting tags
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
# Update list of active formatting tags
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_chord_line += '<td class="chord">%s</td>' % chords[i]
# Check if this is the last column, if so skip spacing calc and instead insert a single space
if i + 1 == len(lyrics):
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * int(math.ceil(spacer / 2))
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
endtags=end_formatting_tags)
new_line += new_chord_line + new_lyric_line + '</tr>'
else:
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_line += '<tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{endtags}</td></tr>'.format(
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
new_line += '</table>'
else:
new_line += line
new_line += '</td></tr></table>'
rendered_text_lines.append(new_line)
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
return ''.join(rendered_text_lines)
def render_tags(text, can_render_chords=False, is_printing=False):
"""
The :func:`~openlp.core.display.render.render_tags` function takes a stirng with OpenLP-style tags in it
and replaces them with the HTML version.
:param str text: The string with OpenLP-style tags to be rendered.
:param bool can_render_chords: Should the chords be rendererd?
:param bool is_printing: Are we going to print this?
:returns str: The HTML version of the tags is returned as a string.
"""
if can_render_chords:
if is_printing:
text = render_chords_for_printing(text, '{br}')
else:
text = render_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
return text

View File

@ -2,9 +2,7 @@ import logging
import os
import json
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, QtWebChannel
from openlp.core.display.webengine import WebEngineView
from PyQt5 import QtCore, QtWidgets, QtWebChannel
log = logging.getLogger(__name__)
@ -79,6 +77,8 @@ class DisplayWindow(QtWidgets.QWidget):
Create the display window
"""
super(DisplayWindow, self).__init__(parent)
# Need to import this inline to get around a QtWebEngine issue
from openlp.core.display.webengine import WebEngineView
self._is_initialised = False
self._fbo = None
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool | QtCore.Qt.WindowStaysOnTopHint)
@ -114,26 +114,6 @@ class DisplayWindow(QtWidgets.QWidget):
"""
self.run_javascript('Display.init();')
def add_script_source(self, fname, source):
"""
Add a script of source code
"""
js = QtWebEngineWidgets.QWebEngineScript()
js.setSourceCode(source)
js.setName(fname)
js.setWorldId(QtWebEngineWidgets.QWebEngineScript.MainWorld)
self.webview.page().scripts().insert(js)
def add_script(self, fname):
"""
Add a script to the page
"""
js_file = QtCore.QFile(fname)
if not js_file.open(QtCore.QIODevice.ReadOnly):
log.warning('Could not open %s: %s', fname, js_file.errorString())
return
self.add_script_source(os.path.basename(fname), str(bytes(js_file.readAll()), 'utf-8'))
def run_javascript(self, script, is_sync=False):
"""
Run some Javascript in the WebView
@ -208,7 +188,7 @@ class DisplayWindow(QtWidgets.QWidget):
"""
Set the playback rate of the current video.
The rate can be any valid float, with 0.0 being stopped, 1.0 being normal speed,
The rate can be any valid float, with 0.0 being stopped, 1.0 being normal speed,
over 1.0 is faster, under 1.0 is slower, and negative is backwards.
:param rate: A float indicating the playback rate.

View File

@ -23,21 +23,16 @@
The :mod:`lib` module contains most of the components and libraries that make
OpenLP work.
"""
import html
import logging
import os
import re
import math
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import translate
from openlp.core.common.path import Path
log = logging.getLogger(__name__ + '.__init__')
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
class ServiceItemContext(object):
"""
@ -287,309 +282,6 @@ def check_item_selected(list_widget, message):
return True
def clean_tags(text, remove_chords=False):
"""
Remove Tags from text for display
:param text: Text to be cleaned
:param remove_chords: Clean ChordPro tags
"""
text = text.replace('<br>', '\n')
text = text.replace('{br}', '\n')
text = text.replace('&nbsp;', ' ')
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], '')
text = text.replace(tag['end tag'], '')
# Remove ChordPro tags
if remove_chords:
text = re.sub(r'\[.+?\]', r'', text)
return text
def expand_tags(text, expand_chord_tags=False, for_printing=False):
"""
Expand tags HTML for display
:param text: The text to be expanded.
"""
if expand_chord_tags:
if for_printing:
text = expand_chords_for_printing(text, '{br}')
else:
text = expand_chords(text)
for tag in FormattingTags.get_html_tags():
text = text.replace(tag['start tag'], tag['start html'])
text = text.replace(tag['end tag'], tag['end html'])
return text
def expand_and_align_chords_in_line(match):
"""
Expand the chords in the line and align them using whitespaces.
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
:param match:
:return: The line with expanded html-chords
"""
whitespaces = ''
chordlen = 0
taillen = 0
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
# The actual chord, would be "G" in match "[G]sweet the "
chord = match.group(1)
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
tail = match.group(2)
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
remainder = match.group(3)
# Line end if found, else None
end = match.group(4)
# Based on char width calculate width of chord
for chord_char in chord:
if chord_char not in SLIMCHARS:
chordlen += 2
else:
chordlen += 1
# Based on char width calculate width of tail
for tail_char in tail:
if tail_char not in SLIMCHARS:
taillen += 2
else:
taillen += 1
# Based on char width calculate width of remainder
for remainder_char in remainder:
if remainder_char not in SLIMCHARS:
taillen += 2
else:
taillen += 1
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
if chordlen >= taillen and end is None:
# Decide if the padding should be "_" for drawing out words or spaces
if tail:
if not remainder:
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
whitespaces += '_'
else:
for c in range(chordlen - taillen + 1):
whitespaces += '&nbsp;'
else:
if not remainder:
for c in range(math.floor((chordlen - taillen) / 2)):
whitespaces += '_'
else:
for c in range(chordlen - taillen + 1):
whitespaces += '&nbsp;'
else:
if not tail and remainder and remainder[0] == ' ':
for c in range(chordlen):
whitespaces += '&nbsp;'
if whitespaces:
if '_' in whitespaces:
ws_length = len(whitespaces)
if ws_length == 1:
whitespaces = '&ndash;'
else:
wsl_mod = ws_length // 2
ws_right = ws_left = ' ' * wsl_mod
whitespaces = ws_left + '&ndash;' + ws_right
whitespaces = '<span class="ws">' + whitespaces + '</span>'
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
whitespaces + html.escape(remainder)
def expand_chords(text):
"""
Expand ChordPro tags
:param text:
"""
text_lines = text.split('{br}')
expanded_text_lines = []
chords_on_prev_line = False
for line in text_lines:
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
if '[' in line and ']' in line:
if chords_on_prev_line:
new_line = '<span class="chordline">'
else:
new_line = '<span class="chordline firstchordline">'
chords_on_prev_line = True
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
expand_and_align_chords_in_line, line)
new_line += '</span>'
expanded_text_lines.append(new_line)
else:
chords_on_prev_line = False
expanded_text_lines.append(html.escape(line))
return '{br}'.join(expanded_text_lines)
def compare_chord_lyric(chord, lyric):
"""
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
:param chord:
:param lyric:
:return:
"""
chordlen = 0
if chord == '&nbsp;':
return 0
chord = re.sub(r'\{.*?\}', r'', chord)
lyric = re.sub(r'\{.*?\}', r'', lyric)
for chord_char in chord:
if chord_char not in SLIMCHARS:
chordlen += 2
else:
chordlen += 1
lyriclen = 0
for lyric_char in lyric:
if lyric_char not in SLIMCHARS:
lyriclen += 2
else:
lyriclen += 1
if chordlen > lyriclen:
return chordlen - lyriclen
else:
return 0
def find_formatting_tags(text, active_formatting_tags):
"""
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
:param text:
:param active_formatting_tags:
:return:
"""
if not re.search(r'\{.*?\}', text):
return active_formatting_tags
word_it = iter(text)
# Loop through lyrics to find any formatting tags
for char in word_it:
if char == '{':
tag = ''
char = next(word_it)
start_tag = True
if char == '/':
start_tag = False
char = next(word_it)
while char != '}':
tag += char
char = next(word_it)
# See if the found tag has an end tag
for formatting_tag in FormattingTags.get_html_tags():
if formatting_tag['start tag'] == '{' + tag + '}':
if formatting_tag['end tag']:
if start_tag:
# prepend the new tag to the list of active formatting tags
active_formatting_tags[:0] = [tag]
else:
# remove the tag from the list
active_formatting_tags.remove(tag)
# Break out of the loop matching the found tag against the tag list.
break
return active_formatting_tags
def expand_chords_for_printing(text, line_split):
"""
Expand ChordPro tags
:param text:
:param line_split:
"""
if not re.search(r'\[.*?\]', text):
return text
text_lines = text.split(line_split)
expanded_text_lines = []
for line in text_lines:
# If a ChordPro is detected in the line, build html tables.
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
active_formatting_tags = []
if re.search(r'\[.*?\]', line):
words = line.split(' ')
in_chord = False
for word in words:
chords = []
lyrics = []
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
# If the word contains a chord, we need to handle it.
if re.search(r'\[.*?\]', word):
chord = ''
lyric = ''
# Loop over each character of the word
for char in word:
if char == '[':
in_chord = True
if lyric != '':
if chord == '':
chord = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
chord = ''
lyric = ''
elif char == ']' and in_chord:
in_chord = False
elif in_chord:
chord += char
else:
lyric += char
if lyric != '' or chord != '':
if chord == '':
chord = '&nbsp;'
if lyric == '':
lyric = '&nbsp;'
chords.append(chord)
lyrics.append(lyric)
new_chord_line = '<tr class="chordrow">'
new_lyric_line = '</tr><tr>'
for i in range(len(lyrics)):
spacer = compare_chord_lyric(chords[i], lyrics[i])
# Handle formatting tags
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
# Update list of active formatting tags
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_chord_line += '<td class="chord">%s</td>' % chords[i]
# Check if this is the last column, if so skip spacing calc and instead insert a single space
if i + 1 == len(lyrics):
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}&nbsp;{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
else:
spacing = ''
if spacer > 0:
space = '&nbsp;' * int(math.ceil(spacer / 2))
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
endtags=end_formatting_tags)
new_line += new_chord_line + new_lyric_line + '</tr>'
else:
start_formatting_tags = ''
if active_formatting_tags:
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
end_formatting_tags = ''
if active_formatting_tags:
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
new_line += '<tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">' \
'{starttags}{lyrics}&nbsp;{endtags}</td></tr>'.format(
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
new_line += '</table>'
else:
new_line += line
new_line += '</td></tr></table>'
expanded_text_lines.append(new_line)
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
return ''.join(expanded_text_lines)
def create_separated_list(string_list):
"""
Returns a string that represents a join of a list of strings with a localized separator.

View File

@ -34,7 +34,8 @@ import ntpath
from PyQt5 import QtGui
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
from openlp.core.display.render import remove_tags, render_tags, render_chords
from openlp.core.lib import ImageSource, build_icon
log = logging.getLogger(__name__)
@ -246,7 +247,7 @@ class ServiceItem(RegistryProperties):
self.renderer.set_item_theme(self.theme)
self.theme_data, self.main, self.footer = self.renderer.pre_render()
if self.service_item_type == ServiceItemType.Text:
expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
can_render_chords = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
'songs/enable chords')
log.debug('Formatting slides: {title}'.format(title=self.title))
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
@ -261,13 +262,13 @@ class ServiceItem(RegistryProperties):
previous_pages[verse_tag] = (slide['raw_slide'], pages)
for page in pages:
page = page.replace('<br>', '{br}')
html_data = expand_tags(page.rstrip(), expand_chord_tags)
html_data = render_tags(page.rstrip(), can_render_chords)
new_frame = {
'title': clean_tags(page),
'text': clean_tags(page.rstrip(), expand_chord_tags),
'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
'title': remove_tags(page),
'text': remove_tags(page.rstrip(), can_render_chords),
'chords_text': render_chords(remove_tags(page.rstrip(), False)),
'html': html_data.replace('&amp;nbsp;', '&nbsp;'),
'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
'printing_html': render_tags(html.escape(page.rstrip()), can_render_chords, is_printing=True),
'verseTag': verse_tag,
}
self._display_frames.append(new_frame)
@ -275,7 +276,7 @@ class ServiceItem(RegistryProperties):
pass
else:
log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
self.title = clean_tags(self.title)
self.title = remove_tags(self.title)
# The footer should never be None, but to be compatible with a few
# nightly builds between 1.9.4 and 1.9.5, we have to correct this to
# avoid tracebacks.

View File

@ -101,7 +101,6 @@ from .filerenameform import FileRenameForm
from .starttimeform import StartTimeForm
from .servicenoteform import ServiceNoteForm
from .serviceitemeditform import ServiceItemEditForm
from .slidecontroller import SlideController, DisplayController, PreviewController, LiveController
from .splashscreen import SplashScreen
from .generaltab import GeneralTab
from .themestab import ThemesTab

View File

@ -42,8 +42,9 @@ from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.path import Path, copyfile, path_to_str, str_to_path
from openlp.core.lib import PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
from openlp.core.lib.ui import create_action
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
ShortcutListForm, FormattingTagForm, PreviewController
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, PluginForm, ShortcutListForm, \
FormattingTagForm
from openlp.core.ui.slidecontroller import LiveController, PreviewController
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.media import MediaController
from openlp.core.ui.printserviceform import PrintServiceForm
@ -51,7 +52,6 @@ from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
from openlp.core.display import Renderer
from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet
from openlp.core.version import get_version
@ -510,7 +510,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Set up the path with plugins
PluginManager(self)
ImageManager()
Renderer()
# Set up the interface
self.setupUi(self)
# Define the media Dock Manager

View File

@ -1,312 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.core.ui.media.webkit` module contains our WebKit video player
"""
import logging
from PyQt5 import QtGui, QtWebEngineWidgets
from openlp.core.common import Settings
from openlp.core.lib import translate
from openlp.core.ui.media import MediaState
from openlp.core.ui.media.mediaplayer import MediaPlayer
log = logging.getLogger(__name__)
VIDEO_CSS = """
#videobackboard {
z-index:3;
background-color: %(bgcolor)s;
}
#video {
background-color: %(bgcolor)s;
z-index:4;
}
"""
VIDEO_JS = """
function show_video(state, path, volume, variable_value){
// Sometimes video.currentTime stops slightly short of video.duration and video.ended is intermittent!
var video = document.getElementById('video');
if(volume != null){
video.volume = volume;
}
switch(state){
case 'load':
video.src = 'file:///' + path;
video.load();
break;
case 'play':
video.play();
break;
case 'pause':
video.pause();
break;
case 'stop':
show_video('pause');
video.currentTime = 0;
break;
case 'close':
show_video('stop');
video.src = '';
break;
case 'length':
return video.duration;
case 'current_time':
return video.currentTime;
case 'seek':
video.currentTime = variable_value;
break;
case 'isEnded':
return video.ended;
case 'setVisible':
video.style.visibility = variable_value;
break;
case 'setBackBoard':
var back = document.getElementById('videobackboard');
back.style.visibility = variable_value;
break;
}
}
"""
VIDEO_HTML = """
<div id="videobackboard" class="size" style="visibility:hidden"></div>
<video id="video" class="size" style="visibility:hidden" autobuffer preload></video>
"""
VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov',
'*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg',
'*.avi', '*.swf']
AUDIO_EXT = ['*.mp3', '*.ogg']
class WebkitPlayer(MediaPlayer):
"""
A specialised version of the MediaPlayer class, which provides a QtWebKit
display.
"""
def __init__(self, parent):
"""
Constructor
"""
super(WebkitPlayer, self).__init__(parent, 'webkit')
self.original_name = 'WebKit'
self.display_name = '&WebKit'
self.parent = parent
self.can_background = True
self.audio_extensions_list = AUDIO_EXT
self.video_extensions_list = VIDEO_EXT
def get_media_display_css(self):
"""
Add css style sheets to htmlbuilder
"""
background = QtGui.QColor(Settings().value('players/background color')).name()
css = VIDEO_CSS % {'bgcolor': background}
return css
def get_media_display_javascript(self):
"""
Add javascript functions to htmlbuilder
"""
return VIDEO_JS
def get_media_display_html(self):
"""
Add html code to htmlbuilder
"""
return VIDEO_HTML
def setup(self, display):
"""
Set up the player
:param display: The display to be updated.
"""
display.web_view.resize(display.size())
display.web_view.raise_()
self.has_own_widget = False
def check_available(self):
"""
Check the availability of the media player.
:return: boolean. True if available
"""
web = QtWebEngineWidgets.QWebEnginePage()
# This script should return '[object HTMLVideoElement]' if the html5 video is available in webkit. Otherwise it
# should return '[object HTMLUnknownElement]'
return web.runJavaScript(
"Object.prototype.toString.call(document.createElement('video'));") == '[object HTMLVideoElement]'
def load(self, display):
"""
Load a video
:param display: The display to be updated.
"""
log.debug('load vid in Webkit Controller')
controller = display.controller
if display.has_audio and not controller.media_info.is_background:
volume = controller.media_info.volume
vol = float(volume) / float(100)
else:
vol = 0
path = controller.media_info.file_info.absoluteFilePath()
display.web_view.setVisible(True)
js = 'show_video("load", "{path}", {vol});'.format(path=path.replace('\\', '\\\\'), vol=str(vol))
display.frame.runJavaScript(js)
return True
def resize(self, display):
"""
Resize the player
:param display: The display to be updated.
"""
display.web_view.resize(display.size())
def play(self, display):
"""
Play a video
:param display: The display to be updated.
"""
controller = display.controller
display.web_loaded = True
start_time = 0
if display.controller.is_live:
if self.get_live_state() != MediaState.Paused and controller.media_info.start_time > 0:
start_time = controller.media_info.start_time
else:
if self.get_preview_state() != MediaState.Paused and controller.media_info.start_time > 0:
start_time = controller.media_info.start_time
self.set_visible(display, True)
display.frame.runJavaScript('show_video("play");')
if start_time > 0:
self.seek(display, controller.media_info.start_time * 1000)
self.set_state(MediaState.Playing, display)
display.web_view.raise_()
return True
def pause(self, display):
"""
Pause a video
:param display: The display to be updated.
"""
display.frame.runJavaScript('show_video("pause");')
self.set_state(MediaState.Paused, display)
def stop(self, display):
"""
Stop a video
:param display: The display to be updated.
"""
display.frame.runJavaScript('show_video("stop");')
self.set_state(MediaState.Stopped, display)
def volume(self, display, volume):
"""
Set the volume
:param display: The display to be updated.
:param volume: The volume to be set.
"""
# 1.0 is the highest value
if display.has_audio:
vol = float(volume) / float(100)
display.frame.runJavaScript('show_video(null, null, %s);' % str(vol))
def seek(self, display, seek_value):
"""
Go to a position in the video
:param display: The display to be updated.
:param seek_value: The value to be set.
"""
seek = float(seek_value) / 1000
display.frame.runJavaScript('show_video("seek", null, null, null, "%f");' % seek)
def reset(self, display):
"""
Reset the player
:param display: The display to be updated.
"""
display.frame.runJavaScript('show_video("close");')
self.set_state(MediaState.Off, display)
def set_visible(self, display, visibility):
"""
Set the visibility
:param display: The display to be updated.
:param visibility: The visibility to be set.
"""
if visibility:
is_visible = "visible"
else:
is_visible = "hidden"
display.frame.runJavaScript('show_video("setVisible", null, null, null, "%s");' % is_visible)
def update_ui(self, display):
"""
Update the UI
:param display: The display to be updated.
"""
controller = display.controller
if display.frame.runJavaScript('show_video("isEnded");'):
self.stop(display)
current_time = display.frame.runJavaScript('show_video("current_time");')
# check if conversion was ok and value is not 'NaN'
if current_time and current_time != float('inf'):
current_time = int(current_time * 1000)
length = display.frame.runJavaScript('show_video("length");')
# check if conversion was ok and value is not 'NaN'
if length and length != float('inf'):
length = int(length * 1000)
if current_time and length:
controller.media_info.length = length
controller.seek_slider.setMaximum(length)
if not controller.seek_slider.isSliderDown():
controller.seek_slider.blockSignals(True)
controller.seek_slider.setSliderPosition(current_time)
controller.seek_slider.blockSignals(False)
def get_info(self):
"""
Return some information about this player
"""
part1 = translate('Media.player', 'Webkit is a media player which runs inside a web browser. This player '
'allows text over video to be rendered.')
part2 = translate('Media.player', 'Audio')
part3 = translate('Media.player', 'Video')
return part1 + '<br/> <strong>' + part2 + '</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' + part3 + \
'</strong><br/>' + str(VIDEO_EXT) + '<br/>'

View File

@ -39,7 +39,7 @@ from openlp.core.lib.ui import create_action
from openlp.core.ui.lib.toolbar import OpenLPToolbar
from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget
from openlp.core.ui import HideMode, DisplayControllerType
from openlp.core.display import MainCanvas, Canvas
from openlp.core.display.window import DisplayWindow
# Threshold which has to be trespassed to toggle.
@ -371,7 +371,7 @@ class SlideController(DisplayController, RegistryProperties):
self.slide_layout.setSpacing(0)
self.slide_layout.setContentsMargins(0, 0, 0, 0)
self.slide_layout.setObjectName('SlideLayout')
self.preview_display = Canvas(self)
self.preview_display = DisplayWindow(self)
self.slide_layout.insertWidget(0, self.preview_display)
self.preview_display.hide()
# Actual preview screen
@ -581,7 +581,7 @@ class SlideController(DisplayController, RegistryProperties):
# rebuild display as screen size changed
if self.display:
self.display.close()
self.display = MainCanvas(self)
self.display = DisplayWindow(self)
self.display.setup()
if self.is_live:
self.__add_actions_to_widget(self.display)

View File

@ -31,8 +31,7 @@ from openlp.core.common import Registry, RegistryProperties, UiStrings, translat
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm
from openlp.core.ui.media.webkitplayer import VIDEO_EXT
from .themewizard import Ui_ThemeWizard
from openlp.core.ui.themewizard import Ui_ThemeWizard
log = logging.getLogger(__name__)

View File

@ -30,8 +30,9 @@ import re
from PyQt5 import QtWidgets
from openlp.core.common import AppLocation, CONTROL_CHARS, Settings
from openlp.core.lib import translate, clean_tags
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
from openlp.core.lib import translate
from openlp.core.display.render import remove_tags
from openlp.plugins.songs.lib.db import Author, MediaFile, Song
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__)
@ -380,7 +381,7 @@ def clean_song(manager, song):
if isinstance(song.lyrics, bytes):
song.lyrics = str(song.lyrics, encoding='utf8')
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1], True)) for verse in verses])
song.search_lyrics = ' '.join([clean_string(remove_tags(verse[1], True)) for verse in verses])
# The song does not have any author, add one.
if not song.authors_songs:
name = SongStrings.AuthorUnknown

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "openlp",
"version": "2.5.1",
"description": "OpenLP is free church worship presentation software",
"repository": "lp:openlp",
"directories": {
"test": "tests"
},
"dependencies": {
"jasmine-core": "^2.6.4",
"karma": "^1.7.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.4",
"phantomjs-prebuilt": "^2.1.14"
},
"scripts": {
"test": "karma start"
},
"author": "OpenLP Developers",
"license": "GPL-2.0",
"devDependencies": {
"karma-log-reporter": "0.0.4"
}
}

View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Test the :mod:`~openlp.core.display.render` package.
"""
from unittest.mock import patch
from openlp.core.display.render import remove_tags, render_tags, render_chords, compare_chord_lyric_width, \
render_chords_for_printing
from openlp.core.lib.formattingtags import FormattingTags
@patch('openlp.core.lib.FormattingTags.get_html_tags')
def test_remove_tags(mocked_get_tags):
"""
Test remove_tags() method.
"""
# GIVEN: Mocked get_html_tags() method.
mocked_get_tags.return_value = [{
'desc': 'Black',
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False
}]
string_to_pass = 'ASDF<br>foo{br}bar&nbsp;{b}black{/b}'
expected_string = 'ASDF\nfoo\nbar black'
# WHEN: Clean the string.
result_string = remove_tags(string_to_pass)
# THEN: The strings should be identical.
assert result_string == expected_string, 'The strings should be identical'
@patch('openlp.core.lib.FormattingTags.get_html_tags')
def test_render_tags(mocked_get_tags):
"""
Test the render_tags() method.
"""
# GIVEN: Mocked get_html_tags() method.
mocked_get_tags.return_value = [
{
'desc': 'Black',
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False
},
{
'desc': 'Yellow',
'start tag': '{y}',
'start html': '<span style="-webkit-text-fill-color:yellow">',
'end tag': '{/y}', 'end html': '</span>', 'protected': True,
'temporary': False
},
{
'desc': 'Green',
'start tag': '{g}',
'start html': '<span style="-webkit-text-fill-color:green">',
'end tag': '{/g}', 'end html': '</span>', 'protected': True,
'temporary': False
}
]
string_to_pass = '{b}black{/b}{y}yellow{/y}'
expected_string = '<span style="-webkit-text-fill-color:black">black</span>' + \
'<span style="-webkit-text-fill-color:yellow">yellow</span>'
# WHEN: Replace the tags.
result_string = render_tags(string_to_pass)
# THEN: The strings should be identical.
assert result_string == expected_string, 'The strings should be identical.'
def test_render_chords():
"""
Test that the rendering of chords works as expected.
"""
# GIVEN: A lyrics-line with chords
text_with_chords = 'H[C]alleluya.[F] [G]'
# WHEN: Expanding the chords
text_with_rendered_chords = render_chords(text_with_chords)
# THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
'&nbsp;&nbsp;</span> <span class="chord"><span><strong>G</strong></span></span></span>'
assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected'
def test_render_chords_with_special_chars(self):
"""
Test that the rendering of chords works as expected when special chars are involved.
"""
# GIVEN: A lyrics-line with chords
text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
# WHEN: Expanding the chords
text_with_rendered_chords = render_tags(text_with_chords, can_render_chords=True)
# THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
'</span>&#x27;M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected'
def test_compare_chord_lyric_short_chord(self):
"""
Test that the chord/lyric comparing works.
"""
# GIVEN: A chord and some lyric
chord = 'C'
lyrics = 'alleluya'
# WHEN: Comparing the chord and lyrics
ret = compare_chord_lyric_width(chord, lyrics)
# THEN: The returned value should 0 because the lyric is longer than the chord
assert ret == 0, 'The returned value should 0 because the lyric is longer than the chord'
def test_compare_chord_lyric_long_chord(self):
"""
Test that the chord/lyric comparing works.
"""
# GIVEN: A chord and some lyric
chord = 'Gsus'
lyrics = 'me'
# WHEN: Comparing the chord and lyrics
ret = compare_chord_lyric_width(chord, lyrics)
# THEN: The returned value should 4 because the chord is longer than the lyric
assert ret == 4, 'The returned value should 4 because the chord is longer than the lyric'
def test_render_chords_for_printing(self):
"""
Test that the rendering of chords for printing works as expected.
"""
# GIVEN: A lyrics-line with chords
text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
FormattingTags.load_tags()
# WHEN: Expanding the chords
text_with_rendered_chords = render_chords_for_printing(text_with_chords, '{br}')
# THEN: We should get html that looks like below
expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
'</td><td class="lyrics">{st}Amazing&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
'<td class="chord">&nbsp;</td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r}&nbsp;{/st}</td></tr></table><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}how&nbsp;{/st}' \
'</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
'sweet&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
'border="0" align="left"><tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td ' \
'class="lyrics">{st}the&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" ' \
'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
'<tr><td class="lyrics">{st}sound&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
'class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st}&nbsp;</td>' \
'</tr></table></td></tr></table>'
assert text_with_rendered_chords == expected_html, 'The rendered chords should look as expected!'

View File

@ -4,11 +4,11 @@ Package to test the openlp.core.lib.htmlbuilder module.
from unittest import TestCase
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtWebKit
from PyQt5 import QtCore
from openlp.core.common import Settings
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
build_lyrics_format_css, build_footer_css, build_chords_css
from openlp.core.lib.theme import HorizontalType, VerticalType
from tests.helpers.testmixin import TestMixin
@ -446,16 +446,6 @@ class Htmbuilder(TestCase, TestMixin):
self.assertEqual(FOOTER_CSS_INVALID, css[0], 'The footer strings should be blank.')
self.assertEqual(FOOTER_CSS_INVALID, css[1], 'The footer strings should be blank.')
def test_webkit_version(self):
"""
Test the webkit_version() function
"""
# GIVEN: Webkit
webkit_ver = float(QtWebKit.qWebKitVersion())
# WHEN: Retrieving the webkit version
# THEN: Webkit versions should match
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
def test_build_chords_css(self):
"""
Test the build_chords_css() function

View File

@ -23,16 +23,14 @@
Package to test the openlp.core.lib package.
"""
import os
from datetime import datetime, timedelta
from unittest import TestCase
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtGui
from openlp.core.common.path import Path
from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \
create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \
get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
from openlp.core.lib import FormattingTags, build_icon, check_item_selected, create_separated_list, create_thumb, \
find_formatting_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
@ -529,67 +527,6 @@ class TestLib(TestCase):
MockedQtWidgets.QMessageBox.information.assert_called_with('parent', 'mocked translate', 'message')
self.assertFalse(result, 'The result should be False')
def test_clean_tags(self):
"""
Test clean_tags() method.
"""
with patch('openlp.core.lib.FormattingTags.get_html_tags') as mocked_get_tags:
# GIVEN: Mocked get_html_tags() method.
mocked_get_tags.return_value = [{
'desc': 'Black',
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False
}]
string_to_pass = 'ASDF<br>foo{br}bar&nbsp;{b}black{/b}'
wanted_string = 'ASDF\nfoo\nbar black'
# WHEN: Clean the string.
result_string = clean_tags(string_to_pass)
# THEN: The strings should be identical.
self.assertEqual(wanted_string, result_string, 'The strings should be identical')
def test_expand_tags(self):
"""
Test the expand_tags() method.
"""
with patch('openlp.core.lib.FormattingTags.get_html_tags') as mocked_get_tags:
# GIVEN: Mocked get_html_tags() method.
mocked_get_tags.return_value = [
{
'desc': 'Black',
'start tag': '{b}',
'start html': '<span style="-webkit-text-fill-color:black">',
'end tag': '{/b}', 'end html': '</span>', 'protected': True,
'temporary': False
},
{
'desc': 'Yellow',
'start tag': '{y}',
'start html': '<span style="-webkit-text-fill-color:yellow">',
'end tag': '{/y}', 'end html': '</span>', 'protected': True,
'temporary': False
},
{
'desc': 'Green',
'start tag': '{g}',
'start html': '<span style="-webkit-text-fill-color:green">',
'end tag': '{/g}', 'end html': '</span>', 'protected': True,
'temporary': False
}
]
string_to_pass = '{b}black{/b}{y}yellow{/y}'
wanted_string = '<span style="-webkit-text-fill-color:black">black</span>' + \
'<span style="-webkit-text-fill-color:yellow">yellow</span>'
# WHEN: Replace the tags.
result_string = expand_tags(string_to_pass)
# THEN: The strings should be identical.
self.assertEqual(wanted_string, result_string, 'The strings should be identical.')
def test_validate_thumb_file_does_not_exist(self):
"""
Test the validate_thumb() function when the thumbnail does not exist
@ -753,67 +690,6 @@ class TestLib(TestCase):
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
'Author 2, and Author 3".')
def test_expand_chords(self):
"""
Test that the expanding of chords works as expected.
"""
# GIVEN: A lyrics-line with chords
text_with_chords = 'H[C]alleluya.[F] [G]'
# WHEN: Expanding the chords
text_with_expanded_chords = expand_chords(text_with_chords)
# THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
'&nbsp;&nbsp;</span> <span class="chord"><span><strong>G</strong></span></span></span>'
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
def test_expand_chords2(self):
"""
Test that the expanding of chords works as expected when special chars are involved.
"""
import html
# GIVEN: A lyrics-line with chords
text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
# WHEN: Expanding the chords
text_with_expanded_chords = expand_tags(text_with_chords, True)
# THEN: We should get html that looks like below
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
'</span>&#x27;M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
def test_compare_chord_lyric_short_chord(self):
"""
Test that the chord/lyric comparing works.
"""
# GIVEN: A chord and some lyric
chord = 'C'
lyrics = 'alleluya'
# WHEN: Comparing the chord and lyrics
ret = compare_chord_lyric(chord, lyrics)
# THEN: The returned value should 0 because the lyric is longer than the chord
self.assertEquals(0, ret, 'The returned value should 0 because the lyric is longer than the chord')
def test_compare_chord_lyric_long_chord(self):
"""
Test that the chord/lyric comparing works.
"""
# GIVEN: A chord and some lyric
chord = 'Gsus'
lyrics = 'me'
# WHEN: Comparing the chord and lyrics
ret = compare_chord_lyric(chord, lyrics)
# THEN: The returned value should 4 because the chord is longer than the lyric
self.assertEquals(4, ret, 'The returned value should 4 because the chord is longer than the lyric')
def test_find_formatting_tags(self):
"""
Test that find_formatting_tags works as expected
@ -828,40 +704,3 @@ class TestLib(TestCase):
# THEN: The list of active tags should contain only 'st'
self.assertListEqual(['st'], active_tags, 'The list of active tags should contain only "st"')
def test_expand_chords_for_printing(self):
"""
Test that the expanding of chords for printing works as expected.
"""
# GIVEN: A lyrics-line with chords
text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
FormattingTags.load_tags()
# WHEN: Expanding the chords
text_with_expanded_chords = expand_chords_for_printing(text_with_chords, '{br}')
# THEN: We should get html that looks like below
expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
'</td><td class="lyrics">{st}Amazing&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
'<td class="chord">&nbsp;</td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r}&nbsp;{/st}</td></tr></table><table ' \
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
'<td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}how&nbsp;{/st}' \
'</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
'sweet&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
'border="0" align="left"><tr class="chordrow"><td class="chord">&nbsp;</td></tr><tr><td ' \
'class="lyrics">{st}the&nbsp;{/st}</td></tr></table><table class="segment" cellpadding="0" ' \
'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
'<tr><td class="lyrics">{st}sound&nbsp;{/st}</td></tr></table><table class="segment" ' \
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
'class="chord">&nbsp;</td></tr><tr><td class="lyrics">{st}&nbsp;{/st}</td></tr></table>' \
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st}&nbsp;</td>' \
'</tr></table></td></tr></table>'
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')

View File

@ -1,207 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Package to test the openlp.core.ui.renderer package.
"""
from unittest import TestCase
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore
from openlp.core.common import Registry
from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
from openlp.core.lib.renderer import words_split, get_start_tags
from openlp.core.lib.theme import Theme
SCREEN = {
'primary': False,
'number': 1,
'size': QtCore.QRect(0, 0, 1024, 768)
}
# WARNING: Leave formatting alone - this is how it's returned in renderer.py
CSS_TEST_ONE = """<!DOCTYPE html><html><head><script>
function show_text(newtext) {
var main = document.getElementById('main');
main.innerHTML = newtext;
// We need to be sure that the page is loaded, that is why we
// return the element's height (even though we do not use the
// returned value).
return main.offsetHeight;
}
</script>
<style>
*{margin: 0; padding: 0; border: 0;}
#main {position: absolute; top: 0px; FORMAT CSS; OUTLINE CSS; }
</style></head>
<body><div id="main"></div></body></html>'"""
class TestRenderer(TestCase):
def setUp(self):
"""
Set up the components need for all tests
"""
# Mocked out desktop object
self.desktop = MagicMock()
self.desktop.primaryScreen.return_value = SCREEN['primary']
self.desktop.screenCount.return_value = SCREEN['number']
self.desktop.screenGeometry.return_value = SCREEN['size']
self.screens = ScreenList.create(self.desktop)
Registry.create()
def tearDown(self):
"""
Delete QApplication.
"""
del self.screens
def test_default_screen_layout(self):
"""
Test the default layout calculations
"""
# GIVEN: A new renderer instance.
renderer = Renderer()
# WHEN: given the default screen size has been created.
# THEN: The renderer have created a default screen.
self.assertEqual(renderer.width, 1024, 'The base renderer should be a live controller')
self.assertEqual(renderer.height, 768, 'The base renderer should be a live controller')
self.assertEqual(renderer.screen_ratio, 0.75, 'The base renderer should be a live controller')
self.assertEqual(renderer.footer_start, 691, 'The base renderer should be a live controller')
@patch('openlp.core.lib.renderer.FormattingTags.get_html_tags')
def test_get_start_tags(self, mocked_get_html_tags):
"""
Test the get_start_tags() method
"""
# GIVEN: A new renderer instance. Broken raw_text (missing closing tags).
given_raw_text = '{st}{r}Text text text'
expected_tuple = ('{st}{r}Text text text{/r}{/st}', '{st}{r}',
'<strong><span style="-webkit-text-fill-color:red">')
mocked_get_html_tags.return_value = [{'temporary': False, 'end tag': '{/r}', 'desc': 'Red',
'start html': '<span style="-webkit-text-fill-color:red">',
'end html': '</span>', 'start tag': '{r}', 'protected': True},
{'temporary': False, 'end tag': '{/st}', 'desc': 'Bold',
'start html': '<strong>', 'end html': '</strong>', 'start tag': '{st}',
'protected': True}]
# WHEN: The renderer converts the start tags
result = get_start_tags(given_raw_text)
# THEN: Check if the correct tuple is returned.
self.assertEqual(result, expected_tuple), 'A tuple should be returned containing the text with correct ' \
'tags, the opening tags, and the opening html tags.'
def test_word_split(self):
"""
Test the word_split() method
"""
# GIVEN: A line of text
given_line = 'beginning asdf \n end asdf'
expected_words = ['beginning', 'asdf', 'end', 'asdf']
# WHEN: Split the line based on word split rules
result_words = words_split(given_line)
# THEN: The word lists should be the same.
self.assertListEqual(result_words, expected_words)
def test_format_slide_logical_split(self):
"""
Test that a line with text and a logic break does not break the renderer just returns the input
"""
# GIVEN: A line of with a space text and the logical split
renderer = Renderer()
renderer.empty_height = 480
given_line = 'a\n[---]\nb'
expected_words = ['a<br>[---]<br>b']
service_item = ServiceItem(None)
# WHEN: Split the line based on word split rules
result_words = renderer.format_slide(given_line, service_item)
# THEN: The word lists should be the same.
self.assertListEqual(result_words, expected_words)
def test_format_slide_blank_before_split(self):
"""
Test that a line with blanks before the logical split at handled
"""
# GIVEN: A line of with a space before the logical split
renderer = Renderer()
renderer.empty_height = 480
given_line = '\n [---]\n'
expected_words = ['<br> [---]']
service_item = ServiceItem(None)
# WHEN: Split the line based on word split rules
result_words = renderer.format_slide(given_line, service_item)
# THEN: The blanks have been removed.
self.assertListEqual(result_words, expected_words)
def test_format_slide_blank_after_split(self):
"""
Test that a line with blanks before the logical split at handled
"""
# GIVEN: A line of with a space after the logical split
renderer = Renderer()
renderer.empty_height = 480
given_line = '\n[---] \n'
expected_words = ['<br>[---] ']
service_item = ServiceItem(None)
# WHEN: Split the line based on word split rules
result_words = renderer.format_slide(given_line, service_item)
# THEN: The blanks have been removed.
self.assertListEqual(result_words, expected_words)
@patch('openlp.core.lib.renderer.QtWebKitWidgets.QWebView')
@patch('openlp.core.lib.renderer.build_lyrics_format_css')
@patch('openlp.core.lib.renderer.build_lyrics_outline_css')
@patch('openlp.core.lib.renderer.build_chords_css')
def test_set_text_rectangle(self, mock_build_chords_css, mock_outline_css, mock_lyrics_css, mock_webview):
"""
Test set_text_rectangle returns a proper html string
"""
# GIVEN: test object and data
mock_lyrics_css.return_value = ' FORMAT CSS; '
mock_outline_css.return_value = ' OUTLINE CSS; '
mock_build_chords_css.return_value = ' CHORDS CSS; '
theme_data = Theme()
theme_data.font_main_name = 'Arial'
theme_data.font_main_size = 20
theme_data.font_main_color = '#FFFFFF'
theme_data.font_main_outline_color = '#FFFFFF'
main = QtCore.QRect(10, 10, 1280, 900)
foot = QtCore.QRect(10, 1000, 1260, 24)
renderer = Renderer()
# WHEN: Calling method
renderer._set_text_rectangle(theme_data=theme_data, rect_main=main, rect_footer=foot)
# THEN: QtWebKitWidgets should be called with the proper string
mock_webview.setHtml.called_with(CSS_TEST_ONE, 'Should be the same')

View File

@ -56,7 +56,6 @@ class TestMainWindow(TestCase, TestMixin):
patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \
patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
self.mocked_settings_form = mocked_settings_form
@ -67,7 +66,6 @@ class TestMainWindow(TestCase, TestMixin):
self.mocked_q_tool_box_class = mocked_q_tool_box_class
self.mocked_add_dock_method = mocked_add_dock_method
self.mocked_theme_manager = mocked_theme_manager
self.mocked_renderer = mocked_renderer
self.main_window = MainWindow()
def tearDown(self):

View File

@ -28,9 +28,9 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtGui
from openlp.core import Registry
from openlp.core.lib import ImageSource, ServiceItemAction
from openlp.core.ui import SlideController, LiveController, PreviewController
from openlp.core.ui.slidecontroller import InfoLabel, WIDE_MENU, NON_TEXT_MENU
from openlp.core.lib import ServiceItemAction
from openlp.core.ui.slidecontroller import WIDE_MENU, NON_TEXT_MENU, InfoLabel, SlideController, LiveController, \
PreviewController
class TestSlideController(TestCase):

View File

@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 OpenLP Developers #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Package to test the openlp.core.ui.media.webkitplayer package.
"""
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core.ui.media.webkitplayer import WebkitPlayer
class TestWebkitPlayer(TestCase):
"""
Test the functions in the :mod:`webkitplayer` module.
"""
def test_check_available_video_disabled(self):
"""
Test of webkit video unavailability
"""
# GIVEN: A WebkitPlayer instance and a mocked QWebPage
mocked_qwebpage = MagicMock()
mocked_qwebpage.mainFrame().runJavaScript.return_value = '[object HTMLUnknownElement]'
with patch('openlp.core.ui.media.webkitplayer.QtWebKitWidgets.QWebPage', **{'return_value': mocked_qwebpage}):
webkit_player = WebkitPlayer(None)
# WHEN: An checking if the player is available
available = webkit_player.check_available()
# THEN: The player should not be available when '[object HTMLUnknownElement]' is returned
self.assertEqual(False, available,
'The WebkitPlayer should not be available when video feature detection fails')
def test_check_available_video_enabled(self):
"""
Test of webkit video availability
"""
# GIVEN: A WebkitPlayer instance and a mocked QWebPage
mocked_qwebpage = MagicMock()
mocked_qwebpage.runJavaScript.return_value = '[object HTMLVideoElement]'
with patch('openlp.core.ui.media.webkitplayer.QtWebKitWidgets.QWebPage', **{'return_value': mocked_qwebpage}):
webkit_player = WebkitPlayer(None)
# WHEN: An checking if the player is available
available = webkit_player.check_available()
# THEN: The player should be available when '[object HTMLVideoElement]' is returned
self.assertEqual(True, available,
'The WebkitPlayer should be available when video feature detection passes')

View File

@ -57,7 +57,6 @@ class TestMainWindow(TestCase, TestMixin):
patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
self.main_window = MainWindow()

View File

@ -25,16 +25,14 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry
from openlp.core.lib import ScreenList, ServiceItem, ItemCapabilities
from openlp.core.ui.mainwindow import MainWindow
from openlp.core.ui.servicemanager import ServiceManagerList
from openlp.core.lib.serviceitem import ServiceItem
from tests.helpers.testmixin import TestMixin
from PyQt5 import QtCore, QtGui, QtWidgets
class TestServiceManager(TestCase, TestMixin):
@ -57,7 +55,6 @@ class TestServiceManager(TestCase, TestMixin):
patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
self.main_window = MainWindow()

View File

@ -0,0 +1,5 @@
// This is a mock QWebChannel
var qt = {webChannelTransport: 1};
var QWebChannel = function (transport, callback) {
callback({objects: {mediaWatcher: {}}});
};

84
tests/js/polyfill.js Normal file
View File

@ -0,0 +1,84 @@
/**
* This file contains polyfills, implementing functionality that exists in WebEngine,
* but isn't yet supported by PhantomJS.
*
* These polyfills have been taken from Mozilla Developer Network.
*/
// Production steps of ECMA-262, Edition 6, 22.1.2.1
if (!Array.from) {
Array.from = (function () {
var toStr = Object.prototype.toString;
var isCallable = function (fn) {
return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
};
var toInteger = function (value) {
var number = Number(value);
if (isNaN(number)) { return 0; }
if (number === 0 || !isFinite(number)) { return number; }
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
};
var maxSafeInteger = Math.pow(2, 53) - 1;
var toLength = function (value) {
var len = toInteger(value);
return Math.min(Math.max(len, 0), maxSafeInteger);
};
// The length property of the from method is 1.
return function from(arrayLike/*, mapFn, thisArg */) {
// 1. Let C be the this value.
var C = this;
// 2. Let items be ToObject(arrayLike).
var items = Object(arrayLike);
// 3. ReturnIfAbrupt(items).
if (arrayLike == null) {
throw new TypeError('Array.from requires an array-like object - not null or undefined');
}
// 4. If mapfn is undefined, then let mapping be false.
var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
var T;
if (typeof mapFn !== 'undefined') {
// 5. else
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
if (!isCallable(mapFn)) {
throw new TypeError('Array.from: when provided, the second argument must be a function');
}
// 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 2) {
T = arguments[2];
}
}
// 10. Let lenValue be Get(items, "length").
// 11. Let len be ToLength(lenValue).
var len = toLength(items.length);
// 13. If IsConstructor(C) is true, then
// 13. a. Let A be the result of calling the [[Construct]] internal method
// of C with an argument list containing the single item len.
// 14. a. Else, Let A be ArrayCreate(len).
var A = isCallable(C) ? Object(new C(len)) : new Array(len);
// 16. Let k be 0.
var k = 0;
// 17. Repeat, while k < len… (also steps a - h)
var kValue;
while (k < len) {
kValue = items[k];
if (mapFn) {
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
} else {
A[k] = kValue;
}
k += 1;
}
// 18. Let putStatus be Put(A, "length", len, true).
A.length = len;
// 20. Return A.
return A;
};
}());
}

597
tests/js/test_display.js Normal file
View File

@ -0,0 +1,597 @@
describe("The enumeration object", function () {
it("BackgroundType should exist", function () {
expect(BackgroundType).toBeDefined();
});
it("GradientType should exist", function () {
expect(GradientType).toBeDefined();
});
it("HorizontalAlign should exist", function () {
expect(HorizontalAlign).toBeDefined();
});
it("VerticalAlign should exist", function () {
expect(VerticalAlign).toBeDefined();
});
it("AudioState should exist", function () {
expect(AudioState).toBeDefined();
});
});
describe("The function", function () {
it("$() should return the right element", function () {
var div = document.createElement("div");
div.setAttribute("id", "dollar-test");
document.body.appendChild(div);
expect($("#dollar-test")[0]).toBe(div);
});
it("_buildLinearGradient() should build the correct string", function () {
var gradient = _buildLinearGradient("left top", "left bottom", "#000", "#fff");
expect(gradient).toBe("-webkit-gradient(linear, left top, left bottom, from(#000), to(#fff)) fixed");
});
it("_buildRadialGradient() should build the correct string", function () {
var gradient = _buildRadialGradient(10, "#000", "#fff");
expect(gradient).toBe("-webkit-gradient(radial, 10 50%, 100, 10 50%, 10, from(#000), to(#fff)) fixed");
});
it("_getStyle should return the correct style on an element", function () {
var div = document.createElement("div");
div.style.setProperty("width", "100px");
div.setAttribute("id", "style-test");
document.body.appendChild(div);
expect(_getStyle($("#style-test")[0], "width")).toBe("100px");
});
it("_nl2br should turn UNIX newlines into <br> tags", function () {
var text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
expect(_nl2br(text)).toEqual("Amazing grace, how sweet the sound<br>That saved a wretch like me");
});
it("_nl2br should turn Windows newlines into <br> tags", function () {
var text = "Amazing grace, how sweet the sound\r\nThat saved a wretch like me";
expect(_nl2br(text)).toEqual("Amazing grace, how sweet the sound<br>That saved a wretch like me");
});
it("_prepareText should turn verse text into a paragraph", function () {
var text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
expect(_prepareText(text)).toEqual("<p>Amazing grace, how sweet the sound<br>That saved a wretch like me</p>");
});
});
describe("The Display object", function () {
it("should start with a blank _slides object", function () {
expect(Display._slides).toEqual({});
});
it("should have the correct Reveal config", function () {
expect(Display._revealConfig).toEqual({
margin: 0.0,
minScale: 1.0,
maxScale: 1.0,
controls: false,
progress: false,
history: false,
overview: false,
center: false,
help: false,
transition: "slide",
backgroundTransition: "fade",
viewDistance: 9999,
width: "100%",
height: "100%"
});
});
it("should have an init() method", function () {
expect(Display.init).toBeDefined();
});
it("should initialise Reveal when init is called", function () {
spyOn(Reveal, "initialize");
Display.init();
expect(Reveal.initialize).toHaveBeenCalled();
});
it("should have a reinit() method", function () {
expect(Display.reinit).toBeDefined();
});
it("should re-initialise Reveal when reinit is called", function () {
spyOn(Reveal, "reinitialize");
Display.reinit();
expect(Reveal.reinitialize).toHaveBeenCalled();
});
it("should have a setTransition() method", function () {
expect(Display.setTransition).toBeDefined();
});
it("should have a correctly functioning setTransition() method", function () {
spyOn(Reveal, "configure");
Display.setTransition("fade");
expect(Reveal.configure).toHaveBeenCalledWith({"transition": "fade"});
});
it("should have a correctly functioning clearSlides() method", function () {
expect(Display.clearSlides).toBeDefined();
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
slidesDiv.innerHTML = "<section><p></p></section>";
document.body.appendChild(slidesDiv);
Display.clearSlides();
expect($(".slides")[0].innerHTML).toEqual("");
expect(Display._slides).toEqual({});
});
it("should have a correct goToSlide() method", function () {
spyOn(Reveal, "slide");
spyOn(Display, "_slides");
Display._slides["v1"] = 0;
Display.goToSlide("v1");
expect(Reveal.slide).toHaveBeenCalledWith(0);
});
});
describe("Display.addTextSlide", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
Display._slides = {};
});
it("should add a new slide", function () {
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
spyOn(Display, "reinit");
Display.addTextSlide(verse, text);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).toHaveBeenCalled();
});
it("should add a new slide without calling reinit()", function () {
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
spyOn(Display, "reinit");
Display.addTextSlide(verse, text, false);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).not.toHaveBeenCalled();
});
it("should update an existing slide", function () {
var verse = "v1", text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", false);
spyOn(Display, "reinit");
Display.addTextSlide(verse, text, true);
expect(Display._slides[verse]).toEqual(0);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section")[0].innerHTML).toEqual(_prepareText(text));
expect(Display.reinit).toHaveBeenCalled();
});
});
describe("Display.setTextSlides", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
Display._slides = {};
});
it("should add a list of slides", function () {
var slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see"
},
{
"verse": "v2",
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
"How precious did that grace appear,\nthe hour I first believed."
}
];
spyOn(Display, "clearSlides");
spyOn(Display, "reinit");
spyOn(Reveal, "slide");
Display.setTextSlides(slides);
expect(Display.clearSlides).toHaveBeenCalledTimes(1);
expect(Display._slides["v1"]).toEqual(0);
expect(Display._slides["v2"]).toEqual(1);
expect($(".slides > section").length).toEqual(2);
expect(Display.reinit).toHaveBeenCalledTimes(1);
expect(Reveal.slide).toHaveBeenCalledWith(0);
});
});
describe("Display.setImageSlides", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
var backgroundDiv = document.createElement("div");
backgroundDiv.setAttribute("id", "global-background");
document.body.appendChild(backgroundDiv);
Display._slides = {};
});
it("should add a list of images", function () {
var slides = [{"file": "file://openlp1.jpg"}, {"file": "file://openlp2.jpg"}];
spyOn(Display, "clearSlides");
spyOn(Display, "reinit");
spyOn(Reveal, "slide");
Display.setImageSlides(slides);
expect(Display.clearSlides).toHaveBeenCalledTimes(1);
expect(Display._slides["0"]).toEqual(0);
expect(Display._slides["1"]).toEqual(1);
expect($(".slides > section").length).toEqual(2);
expect($(".slides > section > img").length).toEqual(2);
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file://openlp1.jpg")
expect($(".slides > section > img")[1].getAttribute("src")).toEqual("file://openlp2.jpg")
expect(Display.reinit).toHaveBeenCalledTimes(1);
expect(Reveal.slide).toHaveBeenCalledWith(0);
});
});
describe("Display.setVideo", function () {
beforeEach(function() {
document.body.innerHTML = "";
var slidesDiv = document.createElement("div");
slidesDiv.setAttribute("class", "slides");
document.body.appendChild(slidesDiv);
var backgroundDiv = document.createElement("div");
backgroundDiv.setAttribute("id", "global-background");
document.body.appendChild(backgroundDiv);
Display._slides = {};
});
it("should add a video to the page", function () {
var video = {"file": "file://video.mp4"};
spyOn(Display, "clearSlides");
spyOn(Display, "reinit");
Display.setVideo(video);
expect(Display.clearSlides).toHaveBeenCalledTimes(1);
expect($(".slides > section").length).toEqual(1);
expect($(".slides > section > video").length).toEqual(1);
expect($(".slides > section > video")[0].src).toEqual("file://video.mp4")
expect(Display.reinit).toHaveBeenCalledTimes(1);
});
});
describe("Display.playVideo", function () {
var playCalled = false,
mockVideo = {
play: function () {
playCalled = true;
}
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should play the video when called", function () {
Display.playVideo();
expect(playCalled).toEqual(true);
});
});
describe("Display.pauseVideo", function () {
var pauseCalled = false,
mockVideo = {
pause: function () {
pauseCalled = true;
}
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should pause the video when called", function () {
Display.pauseVideo();
expect(pauseCalled).toEqual(true);
});
});
describe("Display.stopVideo", function () {
var pauseCalled = false,
mockVideo = {
pause: function () {
pauseCalled = true;
},
currentTime: 10.0
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should play the video when called", function () {
Display.stopVideo();
expect(pauseCalled).toEqual(true);
expect(mockVideo.currentTime).toEqual(0.0);
});
});
describe("Display.seekVideo", function () {
var mockVideo = {
currentTime: 1.0
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should seek to the specified position within the video when called", function () {
Display.seekVideo(7.34);
expect(mockVideo.currentTime).toEqual(7.34);
});
});
describe("Display.setPlaybackRate", function () {
var mockVideo = {
playbackRate: 1.0
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should set the playback rate of the video when called", function () {
// Let's sound like chipmunks!
Display.setPlaybackRate(1.25);
expect(mockVideo.playbackRate).toEqual(1.25);
});
});
describe("Display.setVideoVolume", function () {
var mockVideo = {
volume: 1.0
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should set the correct volume of the video when called", function () {
// Make it quiet
Display.setVideoVolume(30);
expect(mockVideo.volume).toEqual(0.3);
});
});
describe("Display.toggleVideoMute", function () {
var mockVideo = {
muted: false
};
beforeEach(function () {
spyOn(window, "$").and.returnValue([mockVideo]);
});
it("should mute the video when called", function () {
mockVideo.muted = false;
Display.toggleVideoMute();
expect(mockVideo.muted).toEqual(true);
});
it("should unmute the video when called", function () {
mockVideo.muted = true;
Display.toggleVideoMute();
expect(mockVideo.muted).toEqual(false);
});
});
describe("AudioPlayer", function () {
var audioPlayer, audioElement;
beforeEach(function () {
audioElement = {
_eventListeners: {},
_playing: false,
_paused: false,
_stopped: false,
src: "",
addEventListener: function (eventType, listener) {
this._eventListeners[eventType] = this._eventListeners[eventType] || [];
this._eventListeners[eventType].push(listener);
},
play: function () {
this._playing = true;
this._paused = false;
this._stopped = false;
},
pause: function () {
this._playing = false;
this._paused = true;
this._stopped = false;
}
};
spyOn(document, "createElement").and.returnValue(audioElement);
audioPlayer = new AudioPlayer();
});
it("should create an object", function () {
expect(audioPlayer).toBeDefined();
expect(audioPlayer._audioElement).not.toBeNull();
expect(audioPlayer._eventListeners).toEqual({});
expect(audioPlayer._playlist).toEqual([]);
expect(audioPlayer._currentTrack).toEqual(null);
expect(audioPlayer._canRepeat).toEqual(false);
expect(audioPlayer._state).toEqual(AudioState.Stopped);
});
it("should call the correct method when _callListener is called", function () {
var testCalled = false;
function test(event) {
testCalled = true;
}
audioPlayer._eventListeners["test"] = [test];
audioPlayer._callListener({"type": "test"});
expect(testCalled).toEqual(true);
});
it("should log a warning when _callListener is called for an unknown event", function () {
spyOn(console, "warn");
audioPlayer._callListener({"type": "test"});
expect(console.warn).toHaveBeenCalledWith("Received unknown event \"test\", doing nothing.");
});
it("should add all the correct event listeners", function () {
expectedListeners = {
"ended": [audioPlayer.onEnded, audioPlayer._callListener],
"timeupdate": [audioPlayer._callListener],
"volumechange": [audioPlayer._callListener],
"durationchange": [audioPlayer._callListener],
"loadeddata": [audioPlayer._callListener]
};
expect(audioElement._eventListeners).toEqual(expectedListeners);
});
it("should add the correct event listener when calling addEventListener", function () {
function dummy () {};
var expectedListeners = {
"test": [dummy]
};
audioPlayer.addEventListener("test", dummy);
expect(audioPlayer._eventListeners).toEqual(expectedListeners);
});
it("should set call nextTrack when the onEnded listener is called", function () {
spyOn(audioPlayer, "nextTrack");
audioPlayer.onEnded({});
expect(audioPlayer.nextTrack).toHaveBeenCalled();
});
it("should set the _canRepeat property when calling setCanRepeat", function () {
audioPlayer.setCanRepeat(true);
expect(audioPlayer._canRepeat).toEqual(true);
});
it("should clear the playlist when clearTracks is called", function () {
audioPlayer._playlist = ["one", "two", "three"];
audioPlayer.clearTracks();
expect(audioPlayer._playlist).toEqual([]);
});
it("should add a track to the playlist when addTrack is called", function () {
audioPlayer._playlist = [];
audioPlayer.addTrack("one");
expect(audioPlayer._playlist).toEqual(["one"]);
});
it("should move to the first track when canRepeat is true and nextTrack is called", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer.setCanRepeat(true);
audioPlayer._currentTrack = "two";
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("one");
});
it("should move to the next track when nextTrack is called", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer._currentTrack = "one";
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("two");
});
it("should stop when canRepeat is false and nextTrack is called on the last track in the list", function () {
spyOn(audioPlayer, "play");
spyOn(audioPlayer, "stop");
audioPlayer.addTrack("one");
audioPlayer.addTrack("two");
audioPlayer.setCanRepeat(false);
audioPlayer._currentTrack = "two";
audioPlayer.nextTrack();
expect(audioPlayer.play).not.toHaveBeenCalled();
expect(audioPlayer.stop).toHaveBeenCalled();
});
it("should play the first track when nextTrack is called when no songs are playing", function () {
spyOn(audioPlayer, "play");
audioPlayer.addTrack("one");
audioPlayer.nextTrack();
expect(audioPlayer.play).toHaveBeenCalledWith("one");
});
it("should log a warning when nextTrack is called when no songs are in the playlist", function () {
spyOn(console, "warn");
audioPlayer.nextTrack();
expect(console.warn).toHaveBeenCalledWith("No tracks in playlist, doing nothing.");
});
it("should play the track specified when play is called with a filename", function () {
audioPlayer.addTrack("one");
audioPlayer.play("one");
expect(audioPlayer._currentTrack).toEqual("one");
expect(audioElement._playing).toEqual(true);
expect(audioElement.src).toEqual("one");
expect(audioPlayer._state).toEqual(AudioState.Playing);
});
it("should continue playing when play is called without a filename and the player is paused", function () {
audioPlayer._state = AudioState.Paused;
audioPlayer.play();
expect(audioElement._playing).toEqual(true);
expect(audioPlayer._state).toEqual(AudioState.Playing);
});
it("should do nothing when play is called without a filename and the player is not paused", function () {
spyOn(console, "warn");
audioPlayer._state = AudioState.Playing;
audioPlayer.play();
expect(console.warn).toHaveBeenCalledWith("No track currently paused and no track specified, doing nothing.");
});
it("should pause the current track when pause is called", function () {
audioPlayer.pause();
expect(audioPlayer._state).toEqual(AudioState.Paused);
expect(audioElement._paused).toEqual(true);
});
it("should stop the current track when stop is called", function () {
audioPlayer.stop();
expect(audioPlayer._state).toEqual(AudioState.Stopped);
expect(audioElement._paused).toEqual(true);
expect(audioElement.src).toEqual("");
});
});