import QtQuick import QtTest import QtWebEngine import "../../qml" as App Item { id: harness width: 900 height: 650 App.WebPane { id: pane; anchors.fill: parent } TestCase { id: test name: "WebDocumentSecurity" when: windowShown function evaluate(source, world) { var complete = false var output pane.activeView.runJavaScript(source, world === undefined ? WebEngineScript.ApplicationWorld : world, function(result) { output = result; complete = true }) tryVerify(function() { return complete }, 5000) return output } function load(path) { reader.open(path) tryCompare(reader, "loaded", true, 15000) tryVerify(function() { return pane.activeView && pane.activeView.documentReady }, 5000) tryVerify(function() { return reader.index.headings && reader.index.headings.length > 0 }, 5000) } function anchorFraction(saved) { var offset = Number(/:([0-9]+)\)$/.exec(saved.cfi)[1]) var coordinate = /^(vertical|sideways)/.test(saved.writingMode || "") ? "left/innerWidth" : "top/innerHeight" return evaluate("(function(){var n=document.getElementById(" + JSON.stringify(saved.fragment) + ").firstChild;var r=document.createRange();r.setStart(n," + offset + ");r.collapse(true);return r.getBoundingClientRect()." + coordinate + "})()") } function cleanup() { reader.releaseLateResources() reader.deferCommit = false reader.cancelPending() reader.resumeRenderers() } function test_encoded_filename_data() { return [{tag:"spaces", path:"space name.html"}, {tag:"Japanese", path:"日本語.html"}, {tag:"reserved characters", path:"hash#percent%.html"}, {tag:"mixed", path:"日本語 j k 123 #%.html"}] } function test_encoded_filename(data) { load(data.path) compare(evaluate("document.getElementById('value').textContent"), "Correct document") compare(evaluate("decodeURIComponent(location.pathname).slice(1)"), data.path) var original = String(pane.activeView.url) wait(1200) // Canonical comparison must still reject author meta-refresh. compare(String(pane.activeView.url), original) compare(evaluate("document.getElementById('value').textContent"), "Correct document") pane.command("navigate", {url:original + "#target"}) tryVerify(function() { return String(pane.activeView.url).endsWith("#target") }, 3000) tryVerify(function() { return evaluate("scrollY") > 500 }, 3000) } function test_late_resource_restores_logical_character_data() { return [{tag:"decoded image", kind:"image"}, {tag:"font swap", kind:"font"}, {tag:"vertical image", kind:"image", vertical:true}, {tag:"vertical font", kind:"font", vertical:true}, {tag:"native anchor image", kind:"image", nativeAnchor:true}, {tag:"native anchor font", kind:"font", nativeAnchor:true}] } function test_generated_heading_ignores_author_base() { load("base.html") compare(reader.index.headings[0].href, "base.html#one") compare(reader.index.headings[1].href, "base.html#two") compare(reader.index.outline[0].href, "folder/other.html#two") pane.command("heading.index", {index:1}) tryVerify(function() { return evaluate("scrollY") > 1000 }, 3000) verify(String(pane.activeView.url).endsWith("/base.html")) } function test_xhtml_native_and_aria_heading_levels_data() { return [{tag:"XHTML", path:"headings.xhtml", mime:"application/xhtml+xml", nativeTag:"h1"}, {tag:"HTML", path:"headings.html", mime:"text/html", nativeTag:"H1"}] } function test_xhtml_native_and_aria_heading_levels(data) { load(data.path) compare(evaluate("document.contentType"), data.mime) compare(evaluate("document.querySelector('h1').tagName"), data.nativeTag) compare(reader.index.headings.length, 3) compare(reader.index.headings.map(function(row) { return row.level }), [1, 2, 3]) compare(reader.index.headings.map(function(row) { return row.title }), ["Native first", "Native second", "ARIA third"]) compare(reader.index.outline.map(function(row) { return row.depth }), [0, 1, 2]) pane.command("heading.index", {index:0}) tryVerify(function() { return Math.abs(evaluate("document.querySelector('h1').getBoundingClientRect().top")) < 3 }, 3000) pane.command("nav.heading_next", {}) tryVerify(function() { return Math.abs(evaluate("document.querySelector('h2').getBoundingClientRect().top")) < 3 }, 3000) pane.command("nav.heading_previous", {}) tryVerify(function() { return Math.abs(evaluate("document.querySelector('h1').getBoundingClientRect().top")) < 3 }, 3000) verify(String(pane.activeView.url).endsWith("/" + data.path)) } function test_clamped_heading_destinations_are_not_reselected_data() { return [{tag:"horizontal", path:"clamped-headings.xhtml"}, {tag:"vertical-rl", path:"clamped-headings-vertical.xhtml"}] } function test_nested_heading_scroll_counts_visible_movement() { load("nested-headings.xhtml") var result = evaluate("(()=>{var r=__docviewReader.command('nav.heading_next',{});return {result:r,windowY:scrollY,innerY:document.getElementById('scroller').scrollTop}})()") compare(result.result.moved, true) compare(result.result.title, "Inner last") compare(result.windowY, 0) verify(result.innerY > 700) result = evaluate("__docviewReader.command('nav.heading_next',{})") compare(result.moved, false) compare(result.boundary, 1) result = evaluate("__docviewReader.command('nav.heading_previous',{})") compare(result.moved, true) compare(result.title, "Inner first") compare(evaluate("document.getElementById('scroller').scrollTop"), 0) } function test_clamped_heading_destinations_are_not_reselected(data) { load(data.path) compare(reader.index.headings.length, 4) var next = evaluate("(()=>{__docviewReader.command('heading.index',{index:0});return [0,1,2,3].map(()=>{var r=__docviewReader.command('nav.heading_next',{});return {moved:r.moved,boundary:r.boundary||0,title:r.title||'',x:scrollX,y:scrollY}})})()") compare(next[0].title, "Middle") compare(next[1].title, "Last A") verify(next[0].x !== next[1].x || next[0].y !== next[1].y) compare(next[2].moved, false) compare(next[2].boundary, 1) compare(next[3].moved, false) compare(next[3].boundary, 1) compare(next[2].x, next[1].x); compare(next[2].y, next[1].y) var previous = evaluate("[0,1,2].map(()=>{var r=__docviewReader.command('nav.heading_previous',{});return {moved:r.moved,boundary:r.boundary||0,title:r.title||''}})") compare(previous[0].title, "Middle") compare(previous[1].title, "First") compare(previous[2].moved, false) compare(previous[2].boundary, -1) // Scrolling away permits the destination again; extending the // layout makes the two former duplicates independently reachable. var changed = evaluate("(()=>{__docviewReader.command('nav.document_end',{});__docviewReader.command(getComputedStyle(document.body).writingMode==='vertical-rl'?'scroll.right':'scroll.up',{});var resumed=__docviewReader.command('nav.heading_next',{});document.body.style.paddingBlockEnd='900px';var a=__docviewReader.command('nav.heading_next',{}),b=__docviewReader.command('nav.heading_next',{});return {resumed:resumed,a:a,b:b}})()") compare(changed.resumed.moved, true) compare(changed.resumed.title, "Last A") compare(changed.a.moved, true); compare(changed.a.title, "Last A") compare(changed.b.moved, true); compare(changed.b.title, "Last B") } function test_late_resource_restores_logical_character(data) { load(data.vertical ? "late-vertical.html" : "late-layout.html") if (data.nativeAnchor) evaluate("document.documentElement.style.overflowAnchor='auto'") evaluate("document.getElementById('paragraph-40').scrollIntoView()") var saved = evaluate("__docviewReader.location()") var originalOffset = anchorFraction(saved) var extent = data.vertical ? "document.body.scrollWidth" : "document.body.scrollHeight" var originalHeight = evaluate(extent) reader.holdLateResources = true if (data.kind === "image") evaluate("document.getElementById('late-image').src='late-image.svg'") else evaluate("document.body.style.fontFamily='Late, serif'") tryVerify(function() { return reader.delayedResources > 0 }, 5000) reader.releaseLateResources() tryVerify(function() { return data.kind === "image" ? evaluate("document.getElementById('late-image').naturalHeight") === 900 : evaluate("document.fonts.check('14px Late')") }, 5000) tryVerify(function() { return Math.abs(anchorFraction(saved) - originalOffset) < 0.04 }, 5000) verify(evaluate(extent) > originalHeight + 700) compare(evaluate("__docviewReader.location().cfi"), saved.cfi) compare(evaluate("getComputedStyle(document.documentElement).overflowAnchor"), data.nativeAnchor ? "auto" : "none") } function test_late_image_after_continued_scroll() { load("late-layout.html") reader.holdLateResources = true evaluate("document.getElementById('late-image').src='late-image.svg'") tryVerify(function() { return reader.delayedResources > 0 }, 5000) // The final scroll has no intervening location poll. This exercises // the scroll-event path also used by scrollbar dragging/inertia. var saved = evaluate("(function(){document.getElementById('paragraph-40').scrollIntoView();var s=__docviewReader.location();document.getElementById('paragraph-20').scrollIntoView();__docviewReader.location();document.getElementById('paragraph-40').scrollIntoView();return s})()") var offset = anchorFraction(saved) reader.releaseLateResources() tryVerify(function() { return evaluate("document.getElementById('late-image').naturalHeight") === 900 }, 5000) tryVerify(function() { return Math.abs(anchorFraction(saved) - offset) < 0.04 }, 3000) compare(evaluate("__docviewReader.location().cfi"), saved.cfi) } function test_late_image_after_pointer_without_scroll() { load("late-layout.html") evaluate("document.getElementById('paragraph-40').scrollIntoView()") var saved = evaluate("__docviewReader.location()"), offset = anchorFraction(saved) reader.holdLateResources = true evaluate("document.getElementById('late-image').src='late-image.svg'") tryVerify(function() { return reader.delayedResources > 0 }, 5000) mouseClick(pane.activeView, 400, 100) reader.releaseLateResources() tryVerify(function() { return evaluate("document.getElementById('late-image').naturalHeight") === 900 }, 5000) tryVerify(function() { return Math.abs(anchorFraction(saved) - offset) < 0.04 }, 3000) } function test_layout_change_before_location_poll() { load("late-layout.html") evaluate("document.getElementById('paragraph-40').scrollIntoView()") var saved = evaluate("__docviewReader.location()"), offset = anchorFraction(saved) // Model a layout update observed by the heartbeat before the // corresponding image/font completion event is dispatched. evaluate("document.body.style.paddingTop='900px';__docviewReader.location()") tryVerify(function() { return Math.abs(anchorFraction(saved) - offset) < 0.04 }, 3000) compare(evaluate("__docviewReader.location().cfi"), saved.cfi) } function test_late_image_preserves_new_navigation() { load("late-layout.html") evaluate("document.getElementById('paragraph-40').scrollIntoView();__docviewReader.location()") reader.holdLateResources = true evaluate("document.getElementById('late-image').src='late-image.svg'") tryVerify(function() { return reader.delayedResources > 0 }, 5000) pane.command("nav.document_end", {}) tryVerify(function() { return evaluate("__docviewReader.location().progression") > 0.999 }, 5000) var current = evaluate("__docviewReader.location()") reader.releaseLateResources() tryCompare(pane.activeView, "heartbeatPending", 0, 3000) tryVerify(function() { return evaluate("document.getElementById('late-image').naturalHeight") === 900 }, 5000) tryVerify(function() { return evaluate("__docviewReader.location().progression") > 0.999 }, 5000) compare(evaluate("__docviewReader.location().cfi"), current.cfi) } function test_renderer_response_timeout_active_and_reopen() { if (!reader.canPauseRenderers) skip("Real SIGSTOP test requires Linux pidfd") load("reflow.html") var view = pane.activeView, failures = reader.watchdogFailures tryVerify(function() { return reader.hasRenderer(view.documentToken, view.rendererSlot) }, 5000) tryCompare(view, "heartbeatPending", 0, 3000) verify(reader.pauseRenderer(view.documentToken, view.rendererSlot)) pane.heartbeat(view) verify(view.heartbeatPending > 0) var starts = reader.heartbeatStarts, probe = view.heartbeatPending for (var i = 0; i < 10; ++i) pane.heartbeat(view) compare(reader.heartbeatStarts, starts) compare(view.heartbeatPending, probe) tryCompare(reader, "watchdogFailures", failures + 1, 6000) compare(pane.activeView, null) compare(reader.notice, "E_RENDERER_TIMEOUT") load("reflow.html") compare(evaluate("document.querySelector('h1').textContent"), "Logical position") } function test_renderer_response_timeout_pending_preserves_old_document() { if (!reader.canPauseRenderers) skip("Real SIGSTOP test requires Linux pidfd") load("reflow.html") var old = pane.activeView, failures = reader.watchdogFailures reader.deferCommit = true reader.open("vertical.html") tryVerify(function() { return pane.pendingView && pane.pendingView.documentReady }, 5000) var pending = pane.pendingView verify(pending.renderProcessPid !== old.renderProcessPid) tryVerify(function() { return reader.hasRenderer(pending.documentToken, pending.rendererSlot) }, 5000) tryCompare(pending, "heartbeatPending", 0, 3000) verify(reader.pauseRenderer(pending.documentToken, pending.rendererSlot)) pane.heartbeat(pending) tryCompare(reader, "watchdogFailures", failures + 1, 6000) compare(pane.pendingView, null) compare(pane.activeView, old) compare(evaluate("document.querySelector('h1').textContent"), "Logical position") } function test_renderer_response_probe_cancelled_with_pending_document() { if (!reader.canPauseRenderers) skip("Real SIGSTOP test requires Linux pidfd") load("reflow.html") var old = pane.activeView, failures = reader.watchdogFailures reader.deferCommit = true reader.open("vertical.html") tryVerify(function() { return pane.pendingView && pane.pendingView.documentReady }, 5000) var pending = pane.pendingView tryVerify(function() { return reader.hasRenderer(pending.documentToken, pending.rendererSlot) }, 5000) tryCompare(pending, "heartbeatPending", 0, 3000) verify(reader.pauseRenderer(pending.documentToken, pending.rendererSlot)) pane.heartbeat(pending) verify(pending.heartbeatPending > 0) reader.cancelPending() reader.resumeRenderers() wait(2000) compare(reader.watchdogFailures, failures) compare(pane.pendingView, null) compare(pane.activeView, old) compare(evaluate("document.querySelector('h1').textContent"), "Logical position") } function test_spread_pairing_boundaries() { function row(side, policy, layout) { return {href:"p.xhtml",linear:true,layout:layout||"fixed",spread:side||"",renditionSpread:policy||"both"} } var spine = [row("left"),row("right"),row("center"),row("left"),row("right"),row("","none"),row("","both","reflowable")] compare(pane.spreadFor(spine,0,false,true), {left:0,right:1}) compare(pane.spreadFor(spine,1,false,true), {left:0,right:1}) compare(pane.spreadFor(spine,2,false,true), {center:2}) compare(pane.spreadFor(spine,4,false,true), {left:3,right:4}) compare(pane.spreadFor(spine,5,false,true), {center:5}) compare(pane.spreadFor(spine,6,false,true), {center:6}) compare(pane.spreadFor([row("right"),row("left")],0,true,true), {right:0,left:1}) compare(pane.spreadFor([row("left"),row("left"),row("right")],0,false,true), {left:0}) compare(pane.spreadFor([row("left"),row("left"),row("right")],1,false,true), {left:1,right:2}) compare(pane.spreadFor([row("left","landscape"),row("right","landscape")],0,false,false), {center:0}) compare(pane.spreadFor([row("left","auto"),row("right","auto")],0,false,true), {left:0,right:1}) compare(pane.spreadFor([row(),row(),row()],0,false,true), {right:0}) compare(pane.spreadFor([row(),row(),row()],1,false,true), {left:1,right:2}) } function test_resource_issues_count_real_interceptor_denial() { load("index.html") reader.allowNetworkProbe = true pane.activeView.permittedUrl = String(reader.networkProbeUrl) pane.activeView.url = reader.networkProbeUrl tryVerify(function() { return reader.interceptedRequests > 0 }, 5000) compare(reader.forbiddenConnections, 0) reader.allowNetworkProbe = false } function test_resource_issues_count_real_csp_events() { load("index.html") tryVerify(function() { return reader.resourceIssues["csp.style"] >= 1 && reader.resourceIssues["csp.font"] >= 1 && reader.resourceIssues["csp.image"] >= 1 }, 5000) compare(reader.forbiddenConnections, 0) var previous = JSON.stringify(reader.resourceIssues) // Forged events cannot add warnings, including from the privileged // test world. Real events above came from authored resource loads. evaluate("document.dispatchEvent(new SecurityPolicyViolationEvent('securitypolicyviolation',{effectiveDirective:'img-src',disposition:'enforce'}))") wait(500) compare(JSON.stringify(reader.resourceIssues), previous) pane.collectResourceIssues(pane.activeView) wait(100) compare(JSON.stringify(reader.resourceIssues), previous) Object.keys(reader.resourceIssues).forEach(function(key) { verify(/^csp\.(image|style|font|script|frame|connect|form|base|object|media|other)$/.test(key)) verify(Number.isInteger(reader.resourceIssues[key]) && reader.resourceIssues[key] > 0) }) } function test_static_boundary_and_resources() { load("index.html") compare(evaluate("document.getElementById('marker').textContent"), "SCRIPT BLOCKED") compare(evaluate("typeof globalThis.__docviewReader"), "object") var mainWorld = evaluate("typeof globalThis.__docviewReader", WebEngineScript.MainWorld) verify(mainWorld === undefined || mainWorld === "undefined") compare(evaluate("getComputedStyle(document.querySelector('h1')).color"), "rgb(17, 34, 51)") verify(evaluate("document.getElementById('local-image').complete && document.getElementById('local-image').naturalWidth===80")) compare(reader.index.headings.length, 3) compare(reader.index.outline.length, 1) compare(reader.index.headings[2].source, "html-heading") // Observe the authored refresh and any deferred CSS/font requests. wait(1400) verify(String(pane.activeView.url).endsWith("/index.html")) compare(reader.forbiddenConnections, 0) compare(evaluate("document.querySelectorAll('iframe').length"), 2) verify(evaluate("Array.from(document.querySelectorAll('iframe')).every(f=>{try{return !f.contentDocument||!f.contentDocument.body||!f.contentDocument.body.textContent}catch(e){return true}})")) compare(evaluate("document.getElementById('submit').click(); document.getElementById('input').value"), "ORIGINAL") pane.focusContent() evaluate("document.getElementById('input').focus()") keyClick(Qt.Key_X) compare(evaluate("document.getElementById('input').value"), "ORIGINAL") wait(100) compare(reader.forbiddenConnections, 0) } function test_headings_and_cfi_restoration() { load("index.html") var before = evaluate("document.documentElement.outerHTML") pane.command("nav.heading_next", {}) wait(150) pane.command("nav.heading_next", {}) tryVerify(function() { return evaluate("window.scrollY") > 400 }, 3000) var saved = evaluate("__docviewReader.location()") verify(saved.cfi.indexOf("epubcfi(") === 0) verify(saved.progression > 0) pane.command("nav.document_start", {}) tryVerify(function() { return evaluate("window.scrollY") < 10 }, 3000) pane.command("location.restore", saved) tryVerify(function() { return evaluate("window.scrollY") > 400 }, 3000) // Indexing and position helpers do not insert IDs or nodes. compare(evaluate("document.documentElement.outerHTML"), before) } function test_vertical_reading_axis() { load("vertical.html") compare(evaluate("getComputedStyle(document.body).writingMode"), "vertical-rl") pane.command("scroll.half_down", {}) tryVerify(function() { return evaluate("window.scrollX") < -100 }, 3000) verify(evaluate("__docviewReader.location().progression") > 0) pane.command("nav.document_start", {}) tryVerify(function() { return Math.abs(evaluate("window.scrollX")) < 2 }, 3000) } function test_keyboard_link_navigation() { load("index.html") pane.focusContent() pane.command("link.next", {}) tryCompare(pane.activeView, "activeFocus", true, 2000) compare(evaluate("document.activeElement.textContent"), "Second heading") pane.command("link.activate", {}) tryVerify(function() { return String(pane.activeView.url).endsWith("#second") }, 5000) verify(evaluate("window.scrollY") > 400) compare(reader.forbiddenConnections, 0) } function test_link_clear_removes_selection_and_restores_style() { load("index.html") pane.focusContent() var originalUrl = String(pane.activeView.url) var authored = evaluate("(()=>{var a=document.querySelector('nav a');a.style.setProperty('outline-color','red','important');a.style.setProperty('outline-width','5px','important');a.style.setProperty('outline-style','dotted','important');a.style.setProperty('outline-offset','7px','important');return a.getAttribute('style')})()") pane.command("link.next", {}) tryCompare(pane.activeView, "activeFocus", true, 2000) tryVerify(function() { return evaluate("document.activeElement.localName") === "a" }, 3000) pane.command("link.clear", {}) tryVerify(function() { return evaluate("__docviewReader.command('link.activate',{}).empty") === true }, 3000) compare(evaluate("document.querySelector('nav a').getAttribute('style')"), authored) verify(evaluate("document.activeElement.localName") !== "a") pane.command("link.activate", {}) tryCompare(reader, "notice", "Tabでリンクを選択してください", 3000) compare(String(pane.activeView.url), originalUrl) pane.command("link.next", {}) tryVerify(function() { return evaluate("document.activeElement.textContent") === "Second heading" }, 3000) } function test_heading_boundary_is_bound_to_navigation() { load("headings.xhtml") var resource = String(pane.activeView.url) pane.command("navigate", {url:resource, boundary:"heading.last"}) tryVerify(function() { return Math.abs(evaluate("document.getElementById('aria').getBoundingClientRect().top")) < 3 }, 3000) pane.command("navigate", {url:resource, boundary:"heading.first"}) tryVerify(function() { return Math.abs(evaluate("document.querySelector('h1').getBoundingClientRect().top")) < 3 }, 3000) // A replacement navigation must not inherit a previous heading target. pane.command("navigate", {url:resource, boundary:"heading.last"}) pane.command("navigate", {url:resource + "#two"}) tryVerify(function() { return Math.abs(evaluate("document.querySelector('h2').getBoundingClientRect().top")) < 3 }, 3000) compare(pane.activeView.pendingBoundary, "") } function test_typography_preserves_logical_text() { load("reflow.html") var original = evaluate("document.documentElement.outerHTML") compare(evaluate("getComputedStyle(document.body).fontSize"), "14px") evaluate("document.getElementById('paragraph-35').scrollIntoView();scrollBy(0,30)") var saved = evaluate("__docviewReader.location()") verify(saved.textQuote.length > 20) var count = evaluate("document.querySelectorAll('*').length") // Change immediately: restoration must not depend on the polling timer. reader.changeReading(30, 2, false) tryCompare(pane.activeView, "documentReady", true) tryVerify(function() { return evaluate("getComputedStyle(document.body).fontSize") === "30px" }, 5000) wait(250) var after = evaluate("__docviewReader.location()") compare(after.cfi.split(":")[0], saved.cfi.split(":")[0]) verify(Math.abs(anchorFraction(saved) - saved.offsetY) < 0.01, JSON.stringify({before:saved, after:evaluate("__docviewReader.location()"), anchor:anchorFraction(saved)})) compare(evaluate("getComputedStyle(document.getElementById('paragraph-35')).fontSize"), "30px") compare(evaluate("getComputedStyle(document.getElementById('paragraph-35')).lineHeight"), "60px") compare(evaluate("document.querySelectorAll('*').length"), count) reader.changeReading(18, 1.6, true) tryVerify(function() { return evaluate("getComputedStyle(document.body).fontSize") === "14px" }, 5000) wait(200) compare(evaluate("__docviewReader.location().cfi.split(':')[0]"), saved.cfi.split(":")[0]) compare(evaluate("document.documentElement.outerHTML"), original) // Reopen the same authored DOM in a fresh document session. load("reflow.html") pane.command("location.restore", saved) tryVerify(function() { return Math.abs(anchorFraction(saved) - saved.offsetY) < 0.01 }, 5000) pane.command("scroll.half_down", {}) tryVerify(function() { return evaluate("__docviewReader.location().cfi") !== saved.cfi }, 3000) reader.notice = "" pane.command("location.restore", {cfi:"epubcfi(/broken)", fragment:saved.fragment, progression:0.5}) tryVerify(function() { return reader.notice.indexOf("E_LOCATOR_UNRESOLVED") >= 0 }, 3000) } function test_resize_preserves_logical_text() { load("reflow.html") evaluate("document.getElementById('paragraph-42').scrollIntoView();scrollBy(0,25)") var saved = evaluate("__docviewReader.location()") pane.updateLocation(pane.activeView) tryVerify(function() { return pane.activeView.lastLocation && pane.activeView.lastLocation.fragment === saved.fragment }, 3000) var oldWidth = harness.width harness.width = 620 wait(350) verify(Math.abs(anchorFraction(saved) - saved.offsetY) < 0.01, JSON.stringify({before:saved, after:evaluate("__docviewReader.location()"), anchor:anchorFraction(saved)})) harness.width = oldWidth wait(350) verify(Math.abs(anchorFraction(saved) - saved.offsetY) < 0.01, JSON.stringify({before:saved, after:evaluate("__docviewReader.location()"), anchor:anchorFraction(saved)})) } function test_fixed_viewport_ignores_font_override() { load("fixed.html") tryCompare(pane.activeView, "fixedLayout", true) compare(pane.activeView.fixedViewport.width, 800) compare(pane.activeView.fixedViewport.height, 600) verify(Math.abs(pane.activeView.zoomFactor - Math.min(pane.width / 800, pane.height / 600)) < 0.02) verify(Math.abs(evaluate("document.getElementById('frame').getBoundingClientRect().width") - 800) < 0.05) reader.changeReading(40, 2.5, false) wait(250) compare(evaluate("getComputedStyle(document.body).fontSize"), "19px") verify(Math.abs(evaluate("document.getElementById('frame').getBoundingClientRect().height") - 600) < 0.05) } function test_rtl_direction_and_logical_navigation() { load("rtl.html") compare(evaluate("getComputedStyle(document.body).direction"), "rtl") pane.command("scroll.half_down", {}) tryVerify(function() { return evaluate("scrollY") > 100 }, 3000) compare(evaluate("scrollX"), 0) pane.command("nav.document_start", {}) tryVerify(function() { return evaluate("scrollY") < 2 }, 3000) pane.command("nav.heading_next", {}) wait(100) pane.command("nav.heading_next", {}) tryVerify(function() { return evaluate("scrollY") > 1000 }, 3000) compare(evaluate("getComputedStyle(document.body).direction"), "rtl") } function test_search_forward_backward_and_wrap() { load("search.html") var result = null function found(value) { result = {count:value.numberOfMatches, active:value.activeMatch} } pane.activeView.findTextFinished.connect(found) pane.command("search", {query:"needle"}) tryVerify(function() { return result && result.count === 3 && result.active === 1 }, 5000) result = null pane.command("search.next", {}) tryVerify(function() { return result && result.active === 2 }, 5000) result = null pane.command("search.previous", {}) tryVerify(function() { return result && result.active === 1 }, 5000) result = null pane.command("search.previous", {}) tryVerify(function() { return result && result.active === 3 }, 5000) result = null pane.command("search.next", {}) tryVerify(function() { return result && result.active === 1 }, 5000) pane.command("nav.document_start", {}) result = null pane.command("search", {query:"日本語"}) tryVerify(function() { return result && result.count === 2 }, 5000) pane.activeView.findTextFinished.disconnect(found) } function test_search_cancel_discards_completion() { load("search.html") reader.notice = "" var cleared = false function found(value) { if (value.numberOfMatches === 0) cleared = true } pane.activeView.findTextFinished.connect(found) pane.command("search", {query:"not-present-in-this-document"}) pane.command("search.cancel", {}) tryVerify(function() { return cleared }, 5000) wait(100) compare(reader.notice, "") pane.activeView.findTextFinished.disconnect(found) } } }