mirror of
https://github.com/chmouel/liseur.git
synced 2026-10-14 03:39:09 +00:00
This commit is contained in:
parent
5bf98ec353
commit
24f2eb8978
3 changed files with 140 additions and 5 deletions
|
|
@ -269,6 +269,7 @@ dependencies {
|
|||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.rhino)
|
||||
// The app uses the platform's org.json; unit tests need a real implementation
|
||||
// of it because android.jar's is a stub. Test-only, never shipped.
|
||||
testImplementation(libs.json)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import com.chmouel.liseur.translate.PageSentence
|
|||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.RhinoException
|
||||
|
||||
/**
|
||||
* The script the reader sends a translated page to ask whether a tap
|
||||
* landed on the words it marks as the voice's, checked the way
|
||||
* `ImageAtPointTest` checks its script: for what it asks of the
|
||||
* document rather than for what the document makes of the question,
|
||||
* which only a real WebView can say.
|
||||
* The page scripts are checked against the generated code and the
|
||||
* selection range logic runs against a small JavaScript DOM fixture.
|
||||
*/
|
||||
class PageSwapsTest {
|
||||
|
||||
|
|
@ -29,6 +28,132 @@ class PageSwapsTest {
|
|||
.substringAfter("markedAt(url, s, fx, fy) {")
|
||||
.substringBefore("restore() {")
|
||||
|
||||
private fun bodyAfter(script: String, marker: String): String {
|
||||
val bodyStart = script.indexOf(marker) + marker.length
|
||||
var depth = 1
|
||||
var bodyEnd = bodyStart
|
||||
while (depth > 0) {
|
||||
when (script[bodyEnd]) {
|
||||
'{' -> depth++
|
||||
'}' -> depth--
|
||||
}
|
||||
if (depth > 0) bodyEnd++
|
||||
}
|
||||
return script.substring(bodyStart, bodyEnd)
|
||||
}
|
||||
|
||||
private fun selectionIntersects(node: String, start: Int, end: Int): Boolean {
|
||||
val script = PageSwaps.selectionIntersectsAppliedSwap(
|
||||
"https://book/EPUB/text/ch1.xhtml",
|
||||
sentence,
|
||||
)
|
||||
val functionBody = bodyAfter(script, "selectionIntersectsAppliedSwap(url, s) {")
|
||||
val rangeStart = functionBody.indexOf("const range = selection.getRangeAt(0);")
|
||||
val rangeLogic = functionBody.substring(rangeStart)
|
||||
.replace(
|
||||
"for (const [node, extent] of bounds) {",
|
||||
"""
|
||||
for (var boundIndex = 0; boundIndex < bounds.length; boundIndex++) {
|
||||
var node = bounds[boundIndex][0];
|
||||
var extent = bounds[boundIndex][1];
|
||||
""".trimIndent(),
|
||||
)
|
||||
.replace(
|
||||
"for (const edit of edits.get(node) || []) {",
|
||||
"""
|
||||
var nodeEdits = edits.get(node) || [];
|
||||
for (var editIndex = 0; editIndex < nodeEdits.length; editIndex++) {
|
||||
var edit = nodeEdits[editIndex];
|
||||
""".trimIndent(),
|
||||
)
|
||||
.replace(Regex("\\bconst\\b"), "var")
|
||||
val shownBody = bodyAfter(script, "const shown = (node, at) => {")
|
||||
.replace(
|
||||
"for (const e of list) {",
|
||||
"""
|
||||
for (var shownIndex = 0; shownIndex < list.length; shownIndex++) {
|
||||
var e = list[shownIndex];
|
||||
""".trimIndent(),
|
||||
)
|
||||
.replace(Regex("\\bconst\\b"), "var")
|
||||
val context = Context.enter()
|
||||
context.languageVersion = Context.VERSION_ES6
|
||||
var fixture = ""
|
||||
return try {
|
||||
val scope = context.initStandardObjects()
|
||||
fixture = """
|
||||
const translation = "La pluie";
|
||||
const text = { data: "Thus was a La pluie and stormy night.", order: 0 };
|
||||
const note = { data: "[1]", order: 19 };
|
||||
const following = { data: " after", order: note.order + note.data.length };
|
||||
const bounds = [[text, [0, ${sentence.text.length}]]];
|
||||
const edits = {
|
||||
get: function() {
|
||||
return [
|
||||
{ from: 0, to: 2, text: "Thus" },
|
||||
{ from: 9, to: 13, text: translation }
|
||||
];
|
||||
}
|
||||
};
|
||||
function shown(node, at) {
|
||||
$shownBody
|
||||
}
|
||||
function absolute(node, offset) { return node.order + offset; }
|
||||
const Range = { END_TO_START: 1, START_TO_END: 2 };
|
||||
function FakeRange(startNode, startOffset, endNode, endOffset) {
|
||||
this.startNode = startNode;
|
||||
this.startOffset = startOffset;
|
||||
this.endNode = endNode;
|
||||
this.endOffset = endOffset;
|
||||
}
|
||||
FakeRange.prototype.setStart = function(node, offset) {
|
||||
this.startNode = node;
|
||||
this.startOffset = offset;
|
||||
};
|
||||
FakeRange.prototype.setEnd = function(node, offset) {
|
||||
this.endNode = node;
|
||||
this.endOffset = offset;
|
||||
};
|
||||
FakeRange.prototype.compareBoundaryPoints = function(how, other) {
|
||||
if (how === Range.END_TO_START) {
|
||||
return absolute(this.endNode, this.endOffset) - absolute(other.startNode, other.startOffset);
|
||||
}
|
||||
return absolute(this.startNode, this.startOffset) - absolute(other.endNode, other.endOffset);
|
||||
};
|
||||
const selectedRange = new FakeRange(__NODE__, __START__, __NODE__, __END__);
|
||||
const selection = { rangeCount: 1, isCollapsed: false, getRangeAt() { return selectedRange; } };
|
||||
const document = { createRange() { return new FakeRange(null, 0, null, 0); } };
|
||||
(function() {
|
||||
$rangeLogic
|
||||
})();
|
||||
""".trimIndent()
|
||||
.replace("__NODE__", node)
|
||||
.replace("__START__", start.toString())
|
||||
.replace("__END__", end.toString())
|
||||
val result = context.evaluateString(
|
||||
scope,
|
||||
fixture,
|
||||
"PageSwapsTest.js",
|
||||
1,
|
||||
null,
|
||||
)
|
||||
Context.toBoolean(result)
|
||||
} catch (e: RhinoException) {
|
||||
throw AssertionError(
|
||||
"${e.message}: ${e.lineSource() ?: e.lineNumber().let { line -> fixture.lines().getOrNull(line - 1) }}",
|
||||
e,
|
||||
)
|
||||
} finally {
|
||||
Context.exit()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TRANSLATION_NODE = "text"
|
||||
const val NOTE_REFERENCE_NODE = "note"
|
||||
const val FOLLOWING_TEXT_NODE = "following"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `another document's page answers nothing`() {
|
||||
// Readium reuses web views between chapters, so every call names
|
||||
|
|
@ -86,6 +211,13 @@ class PageSwapsTest {
|
|||
assertTrue(body.contains("if (!at) return null;"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dictionary language follows whether selected words were translated`() {
|
||||
assertTrue(selectionIntersects(TRANSLATION_NODE, 12, 15))
|
||||
assertFalse(selectionIntersects(FOLLOWING_TEXT_NODE, 0, 4))
|
||||
assertFalse(selectionIntersects(NOTE_REFERENCE_NODE, 0, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a rect with nothing in it is not a place a finger can land`() {
|
||||
assertTrue(body().contains("r.width > 0 && r.height > 0"))
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ reorderable = "3.1.0"
|
|||
navigation3 = "1.2.0"
|
||||
adaptiveNavigation3 = "1.3.0"
|
||||
robolectric = "4.17"
|
||||
rhino = "1.9.1"
|
||||
# Play build only: on-device translation (see DEVELOPER.md, F-Droid readiness).
|
||||
mlkitTranslate = "17.0.3"
|
||||
junit = "4.13.2"
|
||||
|
|
@ -60,6 +61,7 @@ room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
|||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
androidx-test-core = { module = "androidx.test:core", version = "1.7.0" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue