Made various UI elements collapsible.

master
Pacman Ghost 3 years ago
parent fe45cb6170
commit 68ba40d388
  1. 5
      asl_rulebook2/webapp/search.py
  2. 19
      asl_rulebook2/webapp/startup.py
  3. 15
      asl_rulebook2/webapp/static/ASOP.js
  4. 106
      asl_rulebook2/webapp/static/Collapsible.js
  5. 2
      asl_rulebook2/webapp/static/NavPane.js
  6. 81
      asl_rulebook2/webapp/static/RuleInfo.js
  7. 4
      asl_rulebook2/webapp/static/SearchPane.js
  8. 39
      asl_rulebook2/webapp/static/SearchResult.js
  9. 2
      asl_rulebook2/webapp/static/css/ASOP.css
  10. 4
      asl_rulebook2/webapp/static/css/Collapsible.css
  11. 2
      asl_rulebook2/webapp/static/css/RuleInfo.css
  12. 5
      asl_rulebook2/webapp/static/css/SearchResult.css
  13. BIN
      asl_rulebook2/webapp/static/images/collapse-rulerefs.png
  14. BIN
      asl_rulebook2/webapp/static/images/collapser-down.png
  15. BIN
      asl_rulebook2/webapp/static/images/collapser-up.png
  16. BIN
      asl_rulebook2/webapp/static/images/expand-rulerefs.png
  17. 2
      asl_rulebook2/webapp/templates/index.html
  18. 11
      asl_rulebook2/webapp/tests/fixtures/collapsible/annotations.json
  19. 19
      asl_rulebook2/webapp/tests/fixtures/collapsible/asop/index.json
  20. 3
      asl_rulebook2/webapp/tests/fixtures/collapsible/asop/long-content-0.html
  21. 3
      asl_rulebook2/webapp/tests/fixtures/collapsible/asop/long-content-1.html
  22. 1
      asl_rulebook2/webapp/tests/fixtures/collapsible/asop/short-content-0.html
  23. 1
      asl_rulebook2/webapp/tests/fixtures/collapsible/asop/short-content-1.html
  24. 44
      asl_rulebook2/webapp/tests/fixtures/collapsible/collapsible.index
  25. 12
      asl_rulebook2/webapp/tests/fixtures/collapsible/collapsible.targets
  26. 11
      asl_rulebook2/webapp/tests/fixtures/collapsible/errata/test.json
  27. 25
      asl_rulebook2/webapp/tests/fixtures/collapsible/q+a/test.json
  28. 15
      asl_rulebook2/webapp/tests/test_asop.py
  29. 223
      asl_rulebook2/webapp/tests/test_collapsible.py
  30. 36
      asl_rulebook2/webapp/tests/test_search.py
  31. 2
      asl_rulebook2/webapp/tests/test_sr_filters.py
  32. 3
      conftest.py

@ -667,6 +667,11 @@ def _extract_section_entries( content ):
continue
entry = lxml.html.tostring( elem )
entries.append( entry.decode( "utf-8" ) )
if not entries:
# NOTE: If the content hasn't been divided into entries, we return the whole thing as
# one big entry, which will kinda suck as a search result if it's big, but it's better
# than not seeing anything at all.
return [ content ]
return entries
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -106,13 +106,24 @@ def _do_fixup_content():
@app.route( "/app-config" )
def get_app_config():
"""Return the app config."""
result = {
"capabilities": _capabilities,
}
for key in [ "INITIAL_QUERY_STRING", "DISABLE_AUTO_SHOW_RULE_INFO" ]:
# initialize
_logger.debug( "Sending app config:" )
result = {}
# send the available capabilities
_logger.debug( "- capabilities: %s", _capabilities )
result["capabilities"] = _capabilities
# send any user-defined debug settings
for key in app.config:
if not key.startswith( "WEBAPP_" ):
continue
val = app.config.get( key )
if val is not None:
_logger.debug( "- %s = %s", key, val )
result[ key ] = parse_int( val, val )
return jsonify( result )
# ---------------------------------------------------------------------

@ -17,8 +17,13 @@ gMainApp.component( "asop", {
template: `
<div v-if=isActive :data-chapterid=chapterId id="asop" class="asop" >
<div v-html=title class="title" />
<div v-html=preamble class="preamble" />
<div class="title">
<span v-html=title />
<collapser collapserId="asop-preamble" ref="collapser" />
</div>
<collapsible collapsedHeight=5 ref="collapsible">
<div v-html=preamble class="preamble" />
</collapsible>
<div v-if="sections.length > 0" class="sections" :class="{single: isSingleSection}" ref="sections" >
<div v-for="s in sections" :key=s class="section" v-html=s />
</div>
@ -50,6 +55,9 @@ gMainApp.component( "asop", {
},
mounted() {
// set up the collapser
if ( this.$refs.collapser )
this.$refs.collapser.initCollapser( this.$refs.collapsible, null ) ;
// start off with the intro
this.showIntro() ;
},
@ -57,6 +65,9 @@ gMainApp.component( "asop", {
updated() {
// make the ruleid's clickable
linkifyAutoRuleids( $( this.$el ) ) ;
// update the preamble collapser/collapsible
if ( this.$refs.collapser )
this.$refs.collapser.initCollapser( this.$refs.collapsible, null ) ;
// scroll to the top of the sections each time
if ( this.$refs.sections )
this.$refs.sections.scrollTop = 0 ;

@ -0,0 +1,106 @@
import { gMainApp, gAppConfig, gEventBus } from "./MainApp.js" ;
let gCollapsibleStates = {} ; // nb: we only save the states for the current session
// --------------------------------------------------------------------
gMainApp.component( "collapser", {
props: [ "collapserId" ],
data() { return {
// NOTE: We have to track the collapsed/expanded state here, since there may not be
// an associated collapsible element (it's optional).
// NOTE: This is a tri-state variable (null = don't show)
isCollapsed: null,
// NOTE: We would normally like to have "target" as a property, but a component's $refs
// is only populated after it has been mounted, so you can't have one sub-component refer
// to another one (via its ref) in the template, so the link has to be made in the code,
// in the component's mounted() handler :-/
target: null,
} ; },
template: `<img v-if="isCollapsed != null" :src=getImageUrl() @click=onClick class="collapser" />`,
methods: {
initCollapser( collapsible, isCollapsed ) {
// initialize
this.collapsible = collapsible ; // nb: an associated collapsible is optional
if ( isCollapsed != null ) {
// nb: the caller has decided whether or not we should be visible
this.isCollapsed = isCollapsed ;
} else if ( collapsible ) {
// figure out whether or not we should show ourself (based on how much content there is)
let content = $( collapsible.$el ).text() ;
let threshold = gAppConfig.WEBAPP_COLLAPSIBLE_THRESHOLD || 100 ;
this.isCollapsed = (content.length >= threshold) ? false : null ;
}
// restore any previously-saved state
if ( this.isCollapsed != null ) {
if ( this.collapserId !== undefined && gCollapsibleStates[this.collapserId] !== undefined ) {
this.isCollapsed = gCollapsibleStates[ this.collapserId ] ;
this.updateCollapsible() ;
}
}
},
onClick() {
// toggle our collapsed state
this.isCollapsed = ! this.isCollapsed ;
this.updateCollapsible() ;
if ( this.collapserId !== undefined ) {
// save the new state
gCollapsibleStates[ this.collapserId ] = this.isCollapsed ;
}
gEventBus.emit( "collapsible-toggled", this ) ;
},
getImageUrl() {
return gImagesBaseUrl //eslint-disable-line no-undef
+ "collapser-" + (this.isCollapsed ? "down" : "up") + ".png" ;
},
updateCollapsible() {
// force the associated collapsible to update itself
if ( this.collapsible )
this.collapsible.collapser = this ;
},
},
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "collapsible", {
props: [ "collapsedHeight" ],
data() { return {
collapser: null,
} ; },
template: `
<div :style="{height: isCollapsed() ? getCollapsedHeight()+'px' : null}"
:class="{collapsed: isCollapsed()}" class="collapsible"
>
<slot />
</div>`,
methods: {
getCollapsedHeight() {
if ( this.collapsedHeight !== undefined )
return this.collapsedHeight ;
else
return gAppConfig.WEBAPP_COLLAPSIBLE_HEIGHT || 50 ;
},
isCollapsed() {
return this.collapser != null && this.collapser.isCollapsed ;
},
},
} ) ;

@ -31,7 +31,7 @@ gMainApp.component( "nav-pane", {
created() {
gEventBus.on( "show-target", (cdocId, ruleid) => {
if ( gAppConfig.DISABLE_AUTO_SHOW_RULE_INFO )
if ( gAppConfig.WEBAPP_DISABLE_AUTO_SHOW_RULE_INFO )
return ;
// get the Q+A and annotations for the target being opened
// NOTE: Targets are associated with a content set, but the Q+A is global, which is not quite

@ -1,4 +1,4 @@
import { gMainApp, gUrlParams } from "./MainApp.js" ;
import { gMainApp, gEventBus, gUrlParams } from "./MainApp.js" ;
import { linkifyAutoRuleids, fixupSearchHilites, makeImagesZoomable } from "./utils.js" ;
// --------------------------------------------------------------------
@ -21,7 +21,7 @@ gMainApp.component( "rule-info", {
@click="$emit('close')" ref="closeRuleInfoButton"
title="Close the rule info" class="close-rule-info"
/>
<transition :name=ruleInfoTransitionName @after-enter=onAfterEnterRuleInfoTransition >
<transition :name=ruleInfoTransitionName @after-enter=updateCloseButton >
<div v-show="ruleInfo.length > 0" id="rule-info" ref="ruleInfo" >
<div class="content" ref="content">
<div v-for="ri in ruleInfo" :key=ri >
@ -35,6 +35,14 @@ gMainApp.component( "rule-info", {
</transition>
</div>`,
created() {
// NOTE: Toggling collapsible's can cause the v-scrollbar to appear/hide.
gEventBus.on( "collapsible-toggled", () => {
if ( this.ruleInfo.length > 0 )
this.updateCloseButton() ;
} ) ;
},
beforeUpdate() {
// hide the close button until the "enter" transition has completed
let $closeButton = $( this.$refs.closeRuleInfoButton ) ;
@ -47,7 +55,7 @@ gMainApp.component( "rule-info", {
// NOTE: If we're already visible, we don't get the transition, so we force
// post-transition processing manually.
this.$nextTick( () => {
this.onAfterEnterRuleInfoTransition() ;
this.updateCloseButton() ;
this.$refs.ruleInfo.scrollTop = 0 ;
} ) ;
}
@ -61,7 +69,7 @@ gMainApp.component( "rule-info", {
methods: {
onAfterEnterRuleInfoTransition() {
updateCloseButton() {
// FUDGE! We have to wait until the rule info popup is open before we can check
// if it has a v-scrollbar or not, and hence where we should put the close button.
this.$nextTick( () => {
@ -95,34 +103,41 @@ gMainApp.component( "qa-entry", {
template: `
<div class="qa rule-info">
<div class="caption" v-html=fixupHilites(qaEntry.caption) />
<div v-for="content in qaEntry.content" :key=content class="content">
<div v-if="content.question">
<!-- this is a normal question + one or more answers -->
<img :src=questionImageUrl class="icon" />
<div class="question">
<img v-if=content.image :src=makeQAImageUrl(content.image) class="imageZoom" />
<div v-html=content.question />
<div class="caption">
<collapser ref="collapser" />
<span v-html=fixupHilites(qaEntry.caption) />
</div>
<collapsible ref="collapsible" >
<div v-for="content in qaEntry.content" :key=content class="content">
<div v-if="content.question">
<!-- this is a normal question + one or more answers -->
<img :src=questionImageUrl class="icon" />
<div class="question">
<img v-if=content.image :src=makeQAImageUrl(content.image) class="imageZoom" />
<div v-html=content.question />
</div>
<div v-for="answer in content.answers" class="answer" >
<img :src=answerImageUrl :title=answer[1] class="icon" />
<div v-html=answer[0] />
</div>
</div>
<div v-for="answer in content.answers" class="answer" >
<img :src=answerImageUrl :title=answer[1] class="icon" />
<div v-html=answer[0] />
<div v-else>
<!-- this is an informational entry that contains only answers -->
<img :src=infoImageUrl :title="content.answers.length > 0 ? content.answers[0][1] : ''" class="icon" />
<div v-for="answer in content.answers" class="info" >
<div v-html=answer[0] />
</div>
</div>
</div>
<div v-else>
<!-- this is an informational entry that contains only answers -->
<img :src=infoImageUrl :title="content.answers.length > 0 ? content.answers[0][1] : ''" class="icon" />
<div v-for="answer in content.answers" class="info" >
<div v-html=answer[0] />
<div v-if=content.see_other class="see-other" >
See other errata: <span v-html=content.see_other />
</div>
</div>
<div v-if=content.see_other class="see-other" >
See other errata: <span v-html=content.see_other />
</div>
</div>
</collapsible>
</div>`,
mounted() {
// set up the collapser
this.$refs.collapser.initCollapser( this.$refs.collapsible, null ) ;
// make any images that are part of the Q+A entry zoomable
makeImagesZoomable( $(this.$el) ) ;
},
@ -154,15 +169,23 @@ gMainApp.component( "annotation", {
template: `
<div class="anno rule-info">
<div :class=annoType class="caption" >
<collapser ref="collapser" />
<span v-if=anno.ruleid :data-ruleid=anno.ruleid class="auto-ruleid"> {{anno.ruleid}} </span>
<span v-else> (no rule ID) </span>
</div>
<div class="content">
<img :src=makeIconImageUrl() :title=anno.source class="icon" />
<div v-html=anno.content />
</div>
<collapsible ref="collapsible" >
<div class="content">
<img :src=makeIconImageUrl() :title=anno.source class="icon" />
<div v-html=anno.content />
</div>
</collapsible>
</div>`,
mounted() {
// set up the collapser
this.$refs.collapser.initCollapser( this.$refs.collapsible, null ) ;
},
methods: {
makeIconImageUrl() {
if ( this.annoType )

@ -69,8 +69,8 @@ gMainApp.component( "search-box", {
gEventBus.on( "app-loaded", () => {
// check if we should start off with a query (for debugging porpoises)
if ( gAppConfig.INITIAL_QUERY_STRING )
gEventBus.emit( "search-for", gAppConfig.INITIAL_QUERY_STRING ) ;
if ( gAppConfig.WEBAPP_INITIAL_QUERY_STRING )
gEventBus.emit( "search-for", gAppConfig.WEBAPP_INITIAL_QUERY_STRING ) ;
} ) ;
gEventBus.on( "search-done", (showSrCount) => {

@ -1,4 +1,4 @@
import { gMainApp, gEventBus, gUrlParams } from "./MainApp.js" ;
import { gMainApp, gEventBus, gAppConfig } from "./MainApp.js" ;
import { findTargets, getPrimaryTarget, isRuleid, getChapterResource, fixupSearchHilites, hasHilite, hideFootnotes } from "./utils.js" ;
// --------------------------------------------------------------------
@ -15,6 +15,7 @@ gMainApp.component( "index-sr", {
template: `
<div class="index-sr" >
<div v-if="sr.title || sr.subtitle" :style="{background: cssBackground}" class="title" >
<collapser @click=onToggleRulerefs ref="collapser" />
<a v-if=iconUrl href="#" @click=onClickIcon >
<img :src=iconUrl class="icon" />
</a>
@ -22,9 +23,6 @@ gMainApp.component( "index-sr", {
<span v-if=sr.subtitle class="subtitle" v-html=sr.subtitle />
</div>
<div class="body">
<img v-if="expandRulerefs !== null" :src=getToggleRulerefsImageUrl @click=onToggleRulerefs class="toggle-rulerefs"
:title="expandRulerefs ? 'Hide non-matching rule references. ': 'Show all rule references.'"
/>
<div v-if=sr.content class="content" v-html=sr.content />
<div v-if=sr.see_also class="see-also" > See also:
<span v-for="(sa, sa_no) in sr.see_also" >
@ -46,8 +44,10 @@ gMainApp.component( "index-sr", {
created() {
// figure out whether ruleref's should start expanded or collapsed
if ( this.sr.rulerefs === undefined || this.sr.rulerefs.length == 0 || gUrlParams.get( "no-toggle-rulerefs" ) ) {
// there are no ruleref's - don't show the toggle button
if ( gAppConfig.WEBAPP_DISABLE_COLLAPSIBLE_INDEX_SR ) {
// the user has disabled this feature - don't show the collapser
} else if ( this.sr.rulerefs === undefined || this.sr.rulerefs.length == 0 ) {
// there are no ruleref's - don't show the collapser
} else {
// count how many ruleref's have a matching search term
let nHiliteRulerefs = 0 ;
@ -56,7 +56,7 @@ gMainApp.component( "index-sr", {
++ nHiliteRulerefs;
} ) ;
if ( nHiliteRulerefs == this.sr.rulerefs.length ) {
// every ruleref is a match - don't show the toggle button
// every ruleref is a match - don't show the collapser
} else {
// NOTE: We start the ruleref's expanded if one of the important fields has a matching search term.
// The idea is that the index entry is probably one that the user will be interested in (since there is
@ -69,11 +69,10 @@ gMainApp.component( "index-sr", {
}
},
computed: {
getToggleRulerefsImageUrl() {
// return the image URL for the "toggle ruleref's" button
return gImagesBaseUrl + (this.expandRulerefs ? "collapse" : "expand") + "-rulerefs.png" ; //eslint-disable-line no-undef
},
mounted() {
// set up the collapser
let isCollapsed = (this.expandRulerefs == null) ? null : ! this.expandRulerefs ;
this.$refs.collapser.initCollapser( null, isCollapsed ) ;
},
methods: {
@ -100,7 +99,7 @@ gMainApp.component( "index-sr", {
showRuleref( ruleref ) {
// flag whether the ruleref should be shown or hidden
if ( gUrlParams.get( "no-toggle-rulerefs" ) )
if ( gAppConfig.WEBAPP_DISABLE_COLLAPSIBLE_INDEX_SR )
return true ;
return this.expandRulerefs || hasHilite( ruleref.caption ) ;
},
@ -148,10 +147,20 @@ gMainApp.component( "asop-entry-sr", {
template: `
<div class="asop-entry-sr asop" >
<div v-html="sr.caption+' (ASOP)'" @click=onClickCaption class="caption" title="Go to the ASOP" />
<div class="content" v-html=sr.content />
<div class="caption" title="Go to the ASOP" >
<span v-html="sr.caption+' (ASOP)'" @click=onClickCaption />
<collapser ref="collapser" />
</div>
<collapsible ref="collapsible" >
<div class="content" v-html=sr.content />
</collapsible>
</div>`,
mounted() {
// set up the collapser
this.$refs.collapser.initCollapser( this.$refs.collapsible, null ) ;
},
methods: {
onClickCaption() {
gEventBus.emit( "activate-tab", "nav", "asop" ) ;

@ -27,8 +27,10 @@
.asop .hilite { padding: 0 2px ; background: #ffa ; }
.asop .title { border: 1px solid #666 ; border-radius: 2px ; background: #eee ; font-size: 125% ; font-weight: bold ; padding: 2px 5px ; }
.asop .title .collapser { height: 16px ; float: right ; margin: 2px 0 0 5px ; }
.asop .preamble { margin-bottom: 5px ; padding: 2px 5px ; font-size: 90% ; font-style: italic ; }
.asop .collapsible { overflow-y: visible ; }
.asop .preamble i, #asop .preamble em { font-style: normal ; }
.asop .sections { flex-grow: 1 ; overflow-y: auto ; }

@ -0,0 +1,4 @@
.collapser { cursor: pointer ; opacity: 0.3 ; }
.collapsible { overflow-y: hidden ; }
.collapsible.collapsed { mask-image: linear-gradient( to bottom, black 25%, transparent 100% ) ; }

@ -28,6 +28,7 @@
.qa img.icon { float: left ; margin: 0 6px 0 0 ; height: 18px ; }
.qa .content { clear: left ; margin-top: 5px ; }
.qa .caption { margin-bottom: 2px ; background: #60b0ff ; border-radius: 3px ; padding: 2px 5px ; font-weight: bold ; color: #002850 ; }
.qa .collapser { height: 16px ; float: right ; margin: 1px 2px 1px 0 ; }
.qa .question { margin-bottom: 5px ; color: #002850 ; }
.qa .question img { float: right ; margin: 0 0 5px 5px ; height: 100px ; }
.qa .answer { clear: left ; padding-bottom: 2px ; font-style: italic ; color: #0c6905 ; }
@ -43,4 +44,5 @@
.anno .caption { margin-bottom: 2px ; border-radius: 3px ; padding: 2px 5px ; font-weight: bold ; }
.anno .caption.errata { background: #ffc020 ; }
.anno .caption.user-anno { background: #20ff20 ; }
.anno .collapser { height: 16px ; float: right ; margin: 1px 2px 1px 0 ; }
.anno .content { clear: left ; margin-top: 5px ; }

@ -2,6 +2,7 @@
#search-results .index-sr .title { padding: 3px 6px ; font-weight: bold ; border-radius: 3px ; }
#search-results .index-sr .title img.icon { height: 15px ; float: left ; margin-top: 2px ; }
#search-results .index-sr .collapser { height: 16px ; float: right ; margin: 1px 2px 1px 0 ; }
#search-results .index-sr .subtitle { padding: 2px 5px ; font-weight: normal ; font-size: 80% ; font-style: italic ; }
#search-results .index-sr .body { padding: 2px 5px 0 5px ; }
#search-results .index-sr .content { color: #444 ; }
@ -15,5 +16,7 @@
#search-results .index-sr .ruleid a { cursor: pointer ; }
/* nb: the real ASOP background color is #b72754, but it's quite dark */
#search-results .asop-entry-sr .caption { padding: 3px 6px ; font-weight: bold ; border-radius: 3px ; background: #f6edda ; cursor: pointer ; }
#search-results .asop-entry-sr .caption { padding: 3px 6px ; font-weight: bold ; border-radius: 3px ; background: #f6edda ; }
#search-results .asop-entry-sr .caption span { cursor: pointer ; }
#search-results .asop-entry-sr .collapser { height: 16px ; float: right ; margin: 1px 2px 1px 0 ; }
#search-results .asop-entry-sr .content { padding: 5px ; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

@ -20,6 +20,7 @@
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/ASOP.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/TabbedPages.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/Accordian.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/Collapsible.css' ) }}" />
{%if ASOP_CSS_URL%}
<link rel="stylesheet" type="text/css" href="{{ASOP_CSS_URL}}" />
{%endif%}
@ -76,6 +77,7 @@ gGetFootnotesUrl = "{{ url_for( 'get_footnotes' ) }}" ;
<script type="module" src="{{ url_for( 'static', filename='ASOP.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='TabbedPages.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='Accordian.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='Collapsible.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='UserSettings.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='utils.js' ) }}"></script>

@ -0,0 +1,11 @@
[
{ "ruleid": "C.1",
"content": "A short user annotation."
},
{ "ruleid": "C.2",
"content": "A long user annotation: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
]

@ -0,0 +1,19 @@
{
"chapters": [
{ "caption": "No content", "chapter_id": "no-content" },
{ "caption": "Short content", "chapter_id": "short-content",
"sections": [
{ "caption": "Short content" }
]
},
{ "caption": "Long content", "chapter_id": "long-content",
"sections": [
{ "caption": "Long content" }
]
}
]
}

@ -0,0 +1,3 @@
A long preamble.
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

@ -0,0 +1,3 @@
A long ASOP entry.
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

@ -0,0 +1,44 @@
[
{ "title": "Backblast",
"ruleids": [ "C13.8" ],
"rulerefs": [
{ "caption": "HEAT", "ruleids": [ "C13.8" ] },
{ "caption": "RCL", "ruleids": [ "C12.3-.4" ] }
]
},
{ "title": "CCPh",
"subtitle": "Close Combat Phase",
"ruleids": [ "A3.8" ],
"rulerefs": [
{ "caption": "ENEMY Attacks", "ruleids": [ "S11.5" ] },
{ "caption": "dropping SW before CC", "ruleids": [ "A4.43" ] }
]
},
{ "title": "Double Time",
"ruleids": [ "A4.5-.51", "S6.222" ],
"see_also": [ "CX" ],
"content": "Also known as \"running <em>really</em> fast.\"",
"rulerefs": [
{ "caption": "ENEMY Guard Automatic Action", "ruleids": [ "S6.303" ] },
{ "ruleids": [ "C10.3" ] },
{ "caption": "NA in Advance Phase", "ruleids": [ "A4.7" ] },
{ "caption": "'S?' is \"&lt;NA&gt;\"" }
]
},
{ "title": "Firepower",
"content": "The U.S. has lots of this.",
"ruleids": [ "A1.21" ],
"see_also": [ "FP" ]
},
{ "title": "H#",
"subtitle": "HEAT Depletion Number; the number is the Depletion Number, and the superscript following it indicates the first year it applies and a letter indicates the month of that year [EX: A superscript of \"4\" means the vehicle/ordnance has that ammo starting in 1944]",
"ruleids": [ "C8.3" ],
"see_also": [ "HEAT" ]
}
]

@ -0,0 +1,12 @@
{
"A.1": { "caption": "A short Q+A" },
"A.2": { "caption": "A long Q+A" },
"B.1": { "caption": "A short errata." },
"B.2": { "caption": "A long errata." },
"C.1": { "caption": "A short user annotation." },
"C.2": { "caption": "A long user annotation." }
}

@ -0,0 +1,11 @@
[
{ "ruleid": "B.1",
"content": "A short errata."
},
{ "ruleid": "B.2",
"content": "A long errata: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
]

@ -0,0 +1,25 @@
{
"test": [
{ "caption": "A short Q+A",
"ruleids": [ "A.1" ],
"content": [
{ "question": "Some short content.",
"answers": [ ["Yes.","test"] ]
}
]
},
{ "caption": "A long Q+A",
"ruleids": [ "A.2" ],
"content": [
{ "question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"answers": [ ["No.","test"] ]
}
]
}
]
}

@ -25,7 +25,7 @@ def test_asop_nav( webdriver, webapp ):
# check the nav
select_tabbed_page( "nav", "asop" )
nav = _unload_nav( False )
nav = unload_asop_nav( False )
chapters = asop_index["chapters"]
for chapter_no, chapter in enumerate( chapters ):
chapters[ chapter_no ] = {
@ -57,7 +57,7 @@ def test_asop_content( webdriver, webapp ):
webapp.control_tests.set_data_dir( "asop" )
init_webapp( webapp, webdriver )
select_tabbed_page( "nav", "asop" )
nav = _unload_nav( True )
nav = unload_asop_nav( True )
def load_asop_file( fname, as_json ):
"""Load an ASOP data file."""
@ -261,9 +261,10 @@ def test_asop_entries( webdriver, webapp ):
# ---------------------------------------------------------------------
def open_asop_chapter( chapter_id ):
def open_asop_chapter( chapter_id, nav=None ):
"""Open the specified ASOP chapter."""
nav = _unload_nav( True )
if not nav:
nav = unload_asop_nav( True )
for chapter in nav:
if chapter["chapter_id"] == chapter_id:
chapter["elem"].click()
@ -272,13 +273,13 @@ def open_asop_chapter( chapter_id ):
assert False, "Can't find ASOP chapter: "+chapter_id
return None # nb: for pylint :-/
def open_asop_section( chapter_id, section_no ):
def open_asop_section( chapter_id, section_no, nav=None ):
"""Open the specified ASOP section."""
chapter = open_asop_chapter( chapter_id )
chapter = open_asop_chapter( chapter_id, nav )
chapter["sections"][ section_no ]["elem"].click()
wait_for( 2, lambda: find_child( "#asop .sections.single" ) )
def _unload_nav( include_elems ):
def unload_asop_nav( include_elems ):
"""Unload the ASOP nav."""
chapters = []

@ -0,0 +1,223 @@
""" Test collapsible's and collaper's. """
from asl_rulebook2.webapp.tests.utils import init_webapp, select_tabbed_page, \
find_child, find_children, wait_for, wait_for_elem
from asl_rulebook2.webapp.tests.test_search import do_search
from asl_rulebook2.webapp.tests.test_asop import unload_asop_nav, open_asop_chapter
from asl_rulebook2.webapp.tests.utils import has_class
# ---------------------------------------------------------------------
def test_index_sr( webapp, webdriver ):
"""Test collapsible index search results."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
def count_rulerefs( sr_elem ):
# return how many ruleref's the index search result has
rulerefs = find_children( "ul.rulerefs li", sr_elem )
return len( [ r for r in rulerefs if r.is_displayed() ] )
def do_test( query_string, expected_state, expected_rulerefs, expected_rulerefs2 ):
# do the search
results = do_search( query_string )
assert len(results) == 1
# check the collapsed/expanded state
sr_elem = find_child( "#search-results .sr" )
assert _is_collapsed( sr_elem, has_collapsible=False ) == expected_state
# check the number of rulerefs
assert count_rulerefs( sr_elem ) == expected_rulerefs
if expected_state is None:
return
# toggle the state and check the results
find_child( ".collapser", sr_elem ).click()
wait_for( 2, lambda: _is_collapsed( sr_elem, has_collapsible=False ) != expected_state )
assert count_rulerefs( sr_elem ) == expected_rulerefs2
# toggle the state back and check the results
find_child( ".collapser", sr_elem ).click()
wait_for( 2, lambda: _is_collapsed( sr_elem, has_collapsible=False ) == expected_state )
assert count_rulerefs( sr_elem ) == expected_rulerefs
# do the tests
do_test( "CCPh", False, 2, 0 ) # matches the title
do_test( "Combat", False, 2, 0 ) # matches the subtitle
do_test( "running", False, 4, 0 ) # matches the content
do_test( "RCL", True, 1, 2 ) # matches some (but not all) of the ruleref's
do_test( "rcl AND heat", None, 2, None ) # matches all of the ruleref's
do_test( "firepower", None, 0, None ) # has no ruleref's
# ---------------------------------------------------------------------
def test_qa( webapp, webdriver ):
"""Test collapsible Q+A entries."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
# do the tests
_test_collapsible( '"short Q+A"', "A.1", None )
_test_collapsible( '"long Q+A"', "A.2", False )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def test_errata( webapp, webdriver ):
"""Test collapsible errata."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
# do the tests
_test_collapsible( '"short errata"', "B.1", None )
_test_collapsible( '"long errata"', "B.2", False )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def test_user_annotations( webapp, webdriver ):
"""Test collapsible user annotations."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
# do the tests
_test_collapsible( '"short user annotations"', "C.1", None )
_test_collapsible( '"long user annotations"', "C.2", False )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def test_asop_sr( webapp, webdriver ):
"""Test ASOP search results."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
# do the tests
_test_collapsible( '"short ASOP"', None, None )
_test_collapsible( '"long ASOP"', None, False )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _test_collapsible( query_string, ruleid, expected ):
# do the search
if find_child( "#rule-info" ).is_displayed():
find_child( ".close-rule-info" ).click()
results = do_search( query_string )
assert len(results) == 1
sr_elem = find_child( "#search-results .sr" )
assert _is_collapsed( sr_elem ) == expected
# toggle the state and check the results
if expected is not None:
find_child( ".collapser", sr_elem ).click()
wait_for( 2, lambda: _is_collapsed( sr_elem ) != expected )
find_child( ".collapser", sr_elem ).click()
wait_for( 2, lambda: _is_collapsed( sr_elem ) == expected )
# check if there will be an entry in the rule info popup
if not ruleid:
return
# yup - bring that entry up and check it
results = do_search( ruleid )
popup = wait_for_elem( 2, "#rule-info" )
elems = find_children( ".rule-info", popup )
assert len(elems) == 1
elem = elems[0]
assert _is_collapsed( elem ) == expected
# toggle the state and check the results
if expected is not None:
find_child( ".collapser", elem ).click()
wait_for( 2, lambda: _is_collapsed( elem ) != expected )
find_child( ".collapser", elem ).click()
wait_for( 2, lambda: _is_collapsed( elem ) == expected )
# ---------------------------------------------------------------------
def test_asop_preamble( webapp, webdriver ):
"""Test ASOP preambles."""
# initialize
webapp.control_tests.set_data_dir( "collapsible" )
init_webapp( webapp, webdriver )
select_tabbed_page( "nav", "asop" )
nav = unload_asop_nav( True )
def check_preamble( expected ):
# check if the preamble is collapsed/expanded
elem = find_child( "#asop" )
assert _is_collapsed( elem, has_collapsible=True ) == expected
# open the "no content" section
open_asop_chapter( "no-content", nav )
check_preamble( None )
# open the "short content" section
open_asop_chapter( "short-content", nav )
check_preamble( None )
# open the "long content" section
open_asop_chapter( "long-content", nav )
check_preamble( False )
# collapse the preamble
find_child( "#asop .collapser" ).click()
wait_for( 2, lambda: _is_collapsed( find_child("#asop") ) )
# nb: the preamble is now collapsed
# open the "short content" section
open_asop_chapter( "short-content", nav )
check_preamble( None )
# check that the preamble is still collapsed in the "long content" section
open_asop_chapter( "long-content", nav )
check_preamble( True )
# switch to another nav pane, then check that the preamble is still collapsed when we come back
open_asop_chapter( "short-content", nav )
select_tabbed_page( "nav", "search" )
select_tabbed_page( "nav", "asop" )
check_preamble( None )
open_asop_chapter( "long-content", nav )
check_preamble( True )
# ---------------------------------------------------------------------
def _is_collapsed( elem, has_collapsible=True ):
"""Check the state of a collapser and its associated collapsible."""
# get the state of the collapser
collapser = find_child( "img.collapser", elem )
if not collapser:
return None
url = collapser.get_attribute( "src" )
if url.endswith( "collapser-down.png" ):
is_collapsed = True
else:
assert url.endswith( "collapser-up.png" )
is_collapsed = False
# check the state of the associated collapsible
collapsible = find_child( ".collapsible", elem )
if has_collapsible:
if is_collapsed:
assert has_class( collapsible, "collapsed" )
else:
assert not has_class( collapsible, "collapsed" )
else:
assert collapsible is None
return is_collapsed

@ -112,29 +112,6 @@ def test_targets( webapp, webdriver ):
# ---------------------------------------------------------------------
def test_toggle_rulerefs( webapp, webdriver ):
"""Test expanding/collapsing ruleref's."""
# initialize
webapp.control_tests.set_data_dir( "simple" )
init_webapp( webapp, webdriver )
def do_test( query_string, expected ):
results = do_search( query_string )
assert len(results) == 1
sr_elem = find_child( "#search-results .sr" )
assert _is_expanded_rulerefs( sr_elem ) == expected
# do the tests
do_test( "CCPh", True ) # nb: matches the title
do_test( "Combat", True ) # nb: matches the subtitle
do_test( "running", True ) # nb: matches the content
do_test( "RCL", False ) # nb: matches some (but not all) of the ruleref's
do_test( "rcl AND heat", None ) # nb: matches all of the ruleref's
do_test( "firepower", None ) # nb: has no ruleref's
# ---------------------------------------------------------------------
def test_target_search( webapp, webdriver ):
"""Test searching for targets."""
@ -413,16 +390,3 @@ def unload_search_results():
results.append( sr )
return results
# ---------------------------------------------------------------------
def _is_expanded_rulerefs( sr_elem ):
"""Check if ruleref's have been expanded for a search result."""
img = find_child( "img.toggle-rulerefs", sr_elem )
if not img:
return None
url = img.get_attribute( "src" )
if url.endswith( "collapse-rulerefs.png" ):
return True
assert url.endswith( "expand-rulerefs.png" )
return False

@ -11,7 +11,7 @@ def test_sr_filtering( webdriver, webapp ):
# initialize
webapp.control_tests.set_data_dir( "full" )
webapp.control_tests.set_app_config_val( "DISABLE_AUTO_SHOW_RULE_INFO", True )
webapp.control_tests.set_app_config_val( "WEBAPP_DISABLE_AUTO_SHOW_RULE_INFO", True )
init_webapp( webapp, webdriver )
check_sr_filters( [ "index", "qa", "errata", "asop-entry" ] )

@ -119,7 +119,8 @@ def _make_webapp():
# that thread will still be running, loading the old data into the search index and in-memory structures :-/
# We work-around this by forcing an empty environment before starting the webapp server.
app.config.pop( "DATA_DIR", None )
app.config.pop( "INITIAL_QUERY_STRING", None ) # nb: this can also cause problems
app.config.pop( "WEBAPP_INITIAL_QUERY_STRING", None )
app.config.pop( "DISABLE_FIXUP_CONTENT", None )
# NOTE: We run the server thread as a daemon so that it won't prevent the tests from finishing
# when they're done. However, this makes it difficult to know when to shut the server down,
# and, in particular, clean up the gRPC service. We send an EndTests message at the end of each test,

Loading…
Cancel
Save