Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-drag-and-drop-v2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
xblock-drag-and-drop-v2
Commits
8be8937b
Commit
8be8937b
authored
Jan 12, 2016
by
Tim Krones
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #44 from open-craft/keyboard-a11y
Make DnDv2 keyboard-accessible
parents
9a0f8652
dc0a76ba
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
674 additions
and
125 deletions
+674
-125
drag_and_drop_v2/default_data.py
+3
-2
drag_and_drop_v2/drag_and_drop_v2.py
+16
-4
drag_and_drop_v2/public/css/drag_and_drop.css
+61
-5
drag_and_drop_v2/public/js/drag_and_drop.js
+209
-23
drag_and_drop_v2/public/js/view.js
+87
-28
tests/integration/test_base.py
+18
-0
tests/integration/test_interaction.py
+223
-47
tests/integration/test_render.py
+24
-0
tests/unit/test_advanced.py
+22
-6
tests/unit/test_basics.py
+11
-10
No files found.
drag_and_drop_v2/default_data.py
View file @
8be8937b
...
...
@@ -14,8 +14,9 @@ TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer
MIDDLE_ZONE_DESCRIPTION
=
_
(
"Use this zone to associate an item with the middle layer of the triangle."
)
BOTTOM_ZONE_DESCRIPTION
=
_
(
"Use this zone to associate an item with the bottom layer of the triangle."
)
ITEM_INCORRECT_FEEDBACK
=
_
(
"No, this item does not belong here. Try again."
)
ITEM_CORRECT_FEEDBACK
=
_
(
"Correct! This one belongs to {zone}."
)
ITEM_INCORRECT_FEEDBACK
=
_
(
"No, this item does not belong here. Try again."
)
ITEM_NO_ZONE_FEEDBACK
=
_
(
"You silly, there are no zones for this one."
)
START_FEEDBACK
=
_
(
"Drag the items onto the image above."
)
FINISH_FEEDBACK
=
_
(
"Good work! You have completed this drag and drop exercise."
)
...
...
@@ -88,7 +89,7 @@ DEFAULT_DATA = {
{
"displayName"
:
_
(
"I don't belong anywhere"
),
"feedback"
:
{
"incorrect"
:
_
(
"You silly, there are no zones for this one."
)
,
"incorrect"
:
ITEM_NO_ZONE_FEEDBACK
,
"correct"
:
""
},
"zone"
:
"none"
,
...
...
drag_and_drop_v2/drag_and_drop_v2.py
View file @
8be8937b
...
...
@@ -220,7 +220,7 @@ class DragAndDropBlock(XBlock):
@XBlock.json_handler
def
do_attempt
(
self
,
attempt
,
suffix
=
''
):
item
=
next
(
i
for
i
in
self
.
data
[
'items'
]
if
i
[
'id'
]
==
attempt
[
'val'
])
item
=
self
.
_get_item_definition
(
attempt
[
'val'
])
state
=
None
feedback
=
item
[
'feedback'
][
'incorrect'
]
...
...
@@ -228,7 +228,7 @@ class DragAndDropBlock(XBlock):
is_correct
=
False
is_correct_location
=
False
if
'input'
in
attempt
:
if
'input'
in
attempt
:
# Student submitted numerical value for item
state
=
self
.
_get_item_state
()
.
get
(
str
(
item
[
'id'
]))
if
state
:
state
[
'input'
]
=
attempt
[
'input'
]
...
...
@@ -238,7 +238,7 @@ class DragAndDropBlock(XBlock):
feedback
=
item
[
'feedback'
][
'correct'
]
else
:
is_correct
=
False
elif
item
[
'zone'
]
==
attempt
[
'zone'
]:
elif
item
[
'zone'
]
==
attempt
[
'zone'
]:
# Student placed item in zone
is_correct_location
=
True
if
'inputOptions'
in
item
:
# Input value will have to be provided for the item.
...
...
@@ -250,6 +250,7 @@ class DragAndDropBlock(XBlock):
is_correct
=
True
feedback
=
item
[
'feedback'
][
'correct'
]
state
=
{
'zone'
:
attempt
[
'zone'
],
'x_percent'
:
attempt
[
'x_percent'
],
'y_percent'
:
attempt
[
'y_percent'
],
}
...
...
@@ -349,8 +350,13 @@ class DragAndDropBlock(XBlock):
""" Get all user-specific data, and any applicable feedback """
item_state
=
self
.
_get_item_state
()
for
item_id
,
item
in
item_state
.
iteritems
():
definition
=
next
(
i
for
i
in
self
.
data
[
'items'
]
if
str
(
i
[
'id'
])
==
item_id
)
definition
=
self
.
_get_item_definition
(
int
(
item_id
)
)
item
[
'correct_input'
]
=
self
.
_is_correct_input
(
definition
,
item
.
get
(
'input'
))
# If information about zone is missing
# (because exercise was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
if
item
.
get
(
'zone'
)
is
None
:
item
[
'zone'
]
=
definition
.
get
(
'zone'
,
'unknown'
)
is_finished
=
self
.
_is_finished
()
return
{
...
...
@@ -374,6 +380,12 @@ class DragAndDropBlock(XBlock):
return
state
def
_get_item_definition
(
self
,
item_id
):
"""
Returns definition (settings) for item identified by `item_id`.
"""
return
next
(
i
for
i
in
self
.
data
[
'items'
]
if
i
[
'id'
]
==
item_id
)
def
_get_grade
(
self
):
"""
Returns the student's grade for this block.
...
...
drag_and_drop_v2/public/css/drag_and_drop.css
View file @
8be8937b
...
...
@@ -116,7 +116,8 @@
/* Focused option */
.xblock--drag-and-drop
.drag-container
.item-bank
.option
:focus
,
.xblock--drag-and-drop
.drag-container
.item-bank
.option
:hover
{
.xblock--drag-and-drop
.drag-container
.item-bank
.option
:hover
,
.xblock--drag-and-drop
.drag-container
.item-bank
.option
[
aria-grabbed
=
'true'
]
{
outline-width
:
2px
;
outline-style
:
solid
;
outline-offset
:
-4px
;
...
...
@@ -181,7 +182,6 @@
opacity
:
0.5
;
}
/*** Drop Target ***/
.xblock--drag-and-drop
.target
{
display
:
table
;
...
...
@@ -231,7 +231,6 @@
/* W3C */
box-pack
:
center
;
box-align
:
center
;
}
/* Focused zone */
...
...
@@ -263,7 +262,6 @@
/*** FEEDBACK ***/
.xblock--drag-and-drop
.feedback
{
margin-top
:
20px
;
border-top
:
solid
1px
#bdbdbd
;
}
...
...
@@ -301,10 +299,68 @@
font-size
:
18pt
;
}
.xblock--drag-and-drop
.keyboard-help
{
margin-top
:
3px
;
margin-bottom
:
6px
;
}
.xblock--drag-and-drop
.keyboard-help-dialog
{
position
:
fixed
;
left
:
50%
;
top
:
50%
;
width
:
1px
;
height
:
1px
;
z-index
:
1500
;
}
.xblock--drag-and-drop
.modal-window-overlay
{
display
:
none
;
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
100%
;
background
:
#000
;
opacity
:
0.5
;
z-index
:
1500
;
}
.xblock--drag-and-drop
.modal-window
{
display
:
none
;
position
:
absolute
;
width
:
600px
;
max-width
:
90vw
;
height
:
auto
;
transform
:
translate
(
-50%
,
-50%
);
box-sizing
:
border-box
;
box-shadow
:
0
0
7px
rgba
(
0
,
0
,
0
,
0.4
);
border-radius
:
4px
;
padding
:
7px
;
background-color
:
#e5e5e5
;
text-align
:
left
;
direction
:
ltr
;
z-index
:
1500
;
}
.xblock--drag-and-drop
.modal-content
{
border-radius
:
5px
;
background
:
white
;
margin-bottom
:
5px
;
padding
:
5px
;
}
.xblock--drag-and-drop
.modal-content
li
{
margin-left
:
2%
;
}
.xblock--drag-and-drop
.keyboard-help-button
,
.xblock--drag-and-drop
.reset-button
{
cursor
:
pointer
;
float
:
right
;
color
:
#2d74b3
;
}
.xblock--drag-and-drop
.reset-button
{
float
:
right
;
margin-top
:
3px
;
}
...
...
drag_and_drop_v2/public/js/drag_and_drop.js
View file @
8be8937b
...
...
@@ -12,6 +12,18 @@ function DragAndDropBlock(runtime, element, configuration) {
var
state
=
undefined
;
var
__vdom
=
virtualDom
.
h
();
// blank virtual DOM
// Keyboard accessibility
var
ESC
=
27
;
var
RET
=
13
;
var
SPC
=
32
;
var
TAB
=
9
;
var
M
=
77
;
var
QUESTION_MARK
=
63
;
var
placementMode
=
false
;
var
$selectedItem
;
var
$focusedElement
;
var
init
=
function
()
{
// Load the current user state, and load the image, then render the block.
// We load the user state via AJAX rather than passing it in statically (like we do with
...
...
@@ -23,24 +35,96 @@ function DragAndDropBlock(runtime, element, configuration) {
$
.
ajax
(
runtime
.
handlerUrl
(
element
,
'get_user_state'
),
{
dataType
:
'json'
}),
loadBackgroundImage
()
).
done
(
function
(
stateResult
,
bgImg
){
// Render exercise
configuration
.
zones
.
forEach
(
function
(
zone
)
{
computeZoneDimension
(
zone
,
bgImg
.
width
,
bgImg
.
height
);
});
state
=
stateResult
[
0
];
// stateResult is an array of [data, statusText, jqXHR]
migrateState
(
bgImg
.
width
,
bgImg
.
height
);
applyState
();
// Set up event handlers
initDroppable
();
$
(
document
).
on
(
'mousedown touchstart'
,
closePopup
);
$
(
document
).
on
(
'keydown mousedown touchstart'
,
closePopup
);
$
(
document
).
on
(
'keypress'
,
function
(
evt
)
{
runOnKey
(
evt
,
QUESTION_MARK
,
showKeyboardHelp
);
});
$element
.
on
(
'click'
,
'.keyboard-help-button'
,
showKeyboardHelp
);
$element
.
on
(
'keydown'
,
'.keyboard-help-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
showKeyboardHelp
);
});
$element
.
on
(
'click'
,
'.reset-button'
,
resetExercise
);
$element
.
on
(
'keydown'
,
'.reset-button'
,
function
(
evt
)
{
runOnKey
(
evt
,
RET
,
resetExercise
);
});
$element
.
on
(
'click'
,
'.submit-input'
,
submitInput
);
// Indicate that exercise is done loading
publishEvent
({
event_type
:
'xblock.drag-and-drop-v2.loaded'
});
}).
fail
(
function
()
{
$root
.
text
(
gettext
(
"An error occurred. Unable to load drag and drop exercise."
));
});
};
var
runOnKey
=
function
(
evt
,
key
,
handler
)
{
if
(
evt
.
which
===
key
)
{
handler
(
evt
);
}
};
var
keyboardEventDispatcher
=
function
(
evt
)
{
if
(
evt
.
which
===
TAB
)
{
trapFocus
(
evt
);
}
else
if
(
evt
.
which
===
ESC
)
{
hideKeyboardHelp
(
evt
);
}
};
var
trapFocus
=
function
(
evt
)
{
if
(
evt
.
which
===
TAB
)
{
evt
.
preventDefault
();
focusModalButton
();
}
};
var
focusModalButton
=
function
()
{
$root
.
find
(
'.keyboard-help-dialog .modal-dismiss-button '
).
focus
();
};
var
showKeyboardHelp
=
function
(
evt
)
{
evt
.
preventDefault
();
// Show dialog
var
$keyboardHelpDialog
=
$root
.
find
(
'.keyboard-help-dialog'
);
$keyboardHelpDialog
.
find
(
'.modal-window-overlay'
).
show
();
$keyboardHelpDialog
.
find
(
'.modal-window'
).
show
();
// Handle focus
$focusedElement
=
$
(
':focus'
);
focusModalButton
();
// Set up event handlers
$
(
document
).
on
(
'keydown'
,
keyboardEventDispatcher
);
$keyboardHelpDialog
.
find
(
'.modal-dismiss-button'
).
on
(
'click'
,
hideKeyboardHelp
);
};
var
hideKeyboardHelp
=
function
(
evt
)
{
evt
.
preventDefault
();
// Hide dialog
var
$keyboardHelpDialog
=
$root
.
find
(
'.keyboard-help-dialog'
);
$keyboardHelpDialog
.
find
(
'.modal-window-overlay'
).
hide
();
$keyboardHelpDialog
.
find
(
'.modal-window'
).
hide
();
// Handle focus
$focusedElement
.
focus
();
// Remove event handlers
$
(
document
).
off
(
'keydown'
,
keyboardEventDispatcher
);
$keyboardHelpDialog
.
find
(
'.modal-dismiss-button'
).
off
();
};
/** Asynchronously load the main background image used for this block. */
var
loadBackgroundImage
=
function
()
{
var
promise
=
$
.
Deferred
();
...
...
@@ -127,23 +211,63 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var
initDroppable
=
function
()
{
$root
.
find
(
'.zone'
).
droppable
({
accept
:
'.xblock--drag-and-drop .item-bank .option'
,
tolerance
:
'pointer'
,
drop
:
function
(
evt
,
ui
)
{
var
item_id
=
ui
.
draggable
.
data
(
'value'
);
var
zone
=
$
(
this
).
data
(
'zone'
);
var
isCycleKey
=
function
(
evt
)
{
return
!
evt
.
ctrlKey
&&
!
evt
.
metaKey
&&
evt
.
which
===
TAB
;
};
var
isCancelKey
=
function
(
evt
)
{
return
!
evt
.
ctrlKey
&&
!
evt
.
metaKey
&&
evt
.
which
===
ESC
;
};
var
isActionKey
=
function
(
evt
)
{
var
key
=
evt
.
which
;
if
(
evt
.
ctrlKey
||
evt
.
metaKey
)
{
return
key
===
M
;
}
return
key
===
RET
||
key
===
SPC
;
};
var
focusNextZone
=
function
(
evt
,
$currentZone
)
{
if
(
evt
.
shiftKey
)
{
// Going backward
var
isFirstZone
=
$currentZone
.
prev
(
'.zone'
).
length
===
0
;
if
(
isFirstZone
)
{
evt
.
preventDefault
();
$root
.
find
(
'.target .zone'
).
last
().
focus
();
}
}
else
{
// Going forward
var
isLastZone
=
$currentZone
.
next
(
'.zone'
).
length
===
0
;
if
(
isLastZone
)
{
evt
.
preventDefault
();
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
}
};
var
placeItem
=
function
(
$zone
,
$item
)
{
var
item_id
;
var
$anchor
;
if
(
$item
!==
undefined
)
{
item_id
=
$item
.
data
(
'value'
);
// Element was placed using the mouse,
// so use relevant properties of *item* when calculating new position below.
$anchor
=
$item
;
}
else
{
item_id
=
$selectedItem
.
data
(
'value'
);
// Element was placed using the keyboard,
// so use relevant properties of *zone* when calculating new position below.
$anchor
=
$zone
;
}
var
zone
=
$zone
.
data
(
'zone'
);
var
$target_img
=
$root
.
find
(
'.target-img'
);
// Calculate the position of the center of the dropped element relative to
// the image.
var
x_pos
=
(
ui
.
helper
.
offset
().
left
+
(
ui
.
helper
.
outerWidth
()
/
2
)
-
$target_img
.
offset
().
left
)
;
// Calculate the position of the item to place relative to the image.
var
x_pos
=
$anchor
.
offset
().
left
+
(
$anchor
.
outerWidth
()
/
2
)
-
$target_img
.
offset
().
left
;
var
y_pos
=
$anchor
.
offset
().
top
+
(
$anchor
.
outerHeight
()
/
2
)
-
$target_img
.
offset
().
top
;
var
x_pos_percent
=
x_pos
/
$target_img
.
width
()
*
100
;
var
y_pos
=
(
ui
.
helper
.
offset
().
top
+
(
ui
.
helper
.
outerHeight
()
/
2
)
-
$target_img
.
offset
().
top
);
var
y_pos_percent
=
y_pos
/
$target_img
.
height
()
*
100
;
state
.
items
[
item_id
]
=
{
zone
:
zone
,
x_percent
:
x_pos_percent
,
y_percent
:
y_pos_percent
,
submitting_location
:
true
,
...
...
@@ -153,32 +277,75 @@ function DragAndDropBlock(runtime, element, configuration) {
applyState
();
submitLocation
(
item_id
,
zone
,
x_pos_percent
,
y_pos_percent
);
},
0
);
};
var
initDroppable
=
function
()
{
// Set up zones for keyboard interaction
$root
.
find
(
'.zone'
).
each
(
function
()
{
var
$zone
=
$
(
this
);
$zone
.
on
(
'keydown'
,
function
(
evt
)
{
if
(
placementMode
)
{
if
(
isCycleKey
(
evt
))
{
focusNextZone
(
evt
,
$zone
);
}
else
if
(
isCancelKey
(
evt
))
{
evt
.
preventDefault
();
placementMode
=
false
;
releaseItem
(
$selectedItem
);
}
else
if
(
isActionKey
(
evt
))
{
evt
.
preventDefault
();
placementMode
=
false
;
placeItem
(
$zone
);
releaseItem
(
$selectedItem
);
}
}
});
});
// Make zone accept items that are dropped using the mouse
$root
.
find
(
'.zone'
).
droppable
({
accept
:
'.xblock--drag-and-drop .item-bank .option'
,
tolerance
:
'pointer'
,
drop
:
function
(
evt
,
ui
)
{
var
$zone
=
$
(
this
);
var
$item
=
ui
.
helper
;
placeItem
(
$zone
,
$item
);
}
});
};
var
initDraggable
=
function
()
{
$root
.
find
(
'.item-bank .option'
).
not
(
'[data-drag-disabled=true]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
// Allow item to be "picked up" using the keyboard
$item
.
on
(
'keydown'
,
function
(
evt
)
{
if
(
isActionKey
(
evt
))
{
evt
.
preventDefault
();
placementMode
=
true
;
grabItem
(
$item
);
$selectedItem
=
$item
;
$root
.
find
(
'.target .zone'
).
first
().
focus
();
}
});
// Make item draggable using the mouse
try
{
$
(
this
)
.
draggable
({
$
item
.
draggable
({
containment
:
$root
.
find
(
'.xblock--drag-and-drop .drag-container'
),
cursor
:
'move'
,
stack
:
$root
.
find
(
'.xblock--drag-and-drop .item-bank .option'
),
revert
:
'invalid'
,
revertDuration
:
150
,
start
:
function
(
evt
,
ui
)
{
var
item_id
=
$
(
this
).
data
(
'value'
);
setGrabbedState
(
item_id
,
true
);
updateDOM
();
var
$item
=
$
(
this
);
grabItem
(
$item
);
publishEvent
({
event_type
:
'xblock.drag-and-drop-v2.item.picked-up'
,
item_id
:
item_id
item_id
:
$item
.
data
(
'value'
),
});
},
stop
:
function
(
evt
,
ui
)
{
var
item_id
=
$
(
this
).
data
(
'value'
);
setGrabbedState
(
item_id
,
false
);
updateDOM
();
releaseItem
(
$
(
this
));
}
});
}
catch
(
e
)
{
...
...
@@ -188,6 +355,18 @@ function DragAndDropBlock(runtime, element, configuration) {
});
};
var
grabItem
=
function
(
$item
)
{
var
item_id
=
$item
.
data
(
'value'
);
setGrabbedState
(
item_id
,
true
);
updateDOM
();
};
var
releaseItem
=
function
(
$item
)
{
var
item_id
=
$item
.
data
(
'value'
);
setGrabbedState
(
item_id
,
false
);
updateDOM
();
};
var
setGrabbedState
=
function
(
item_id
,
grabbed
)
{
for
(
var
i
=
0
;
i
<
configuration
.
items
.
length
;
i
++
)
{
if
(
configuration
.
items
[
i
].
id
===
item_id
)
{
...
...
@@ -198,8 +377,12 @@ function DragAndDropBlock(runtime, element, configuration) {
var
destroyDraggable
=
function
()
{
$root
.
find
(
'.item-bank .option[data-drag-disabled=true]'
).
each
(
function
()
{
var
$item
=
$
(
this
);
$item
.
off
();
try
{
$
(
this
)
.
draggable
(
'destroy'
);
$
item
.
draggable
(
'destroy'
);
}
catch
(
e
)
{
// Destroying the draggable will fail if draggable was
// not initialized in the first place. Ignore the exception.
...
...
@@ -296,7 +479,8 @@ function DragAndDropBlock(runtime, element, configuration) {
applyState
();
};
var
resetExercise
=
function
()
{
var
resetExercise
=
function
(
evt
)
{
evt
.
preventDefault
();
$
.
ajax
({
type
:
'POST'
,
url
:
runtime
.
handlerUrl
(
element
,
'reset'
),
...
...
@@ -331,10 +515,11 @@ function DragAndDropBlock(runtime, element, configuration) {
if
(
item
.
grabbed
!==
undefined
)
{
grabbed
=
item
.
grabbed
;
}
var
placed
=
item_user_state
&&
(
'input'
in
item_user_state
||
item_user_state
.
correct_input
);
var
itemProperties
=
{
value
:
item
.
id
,
drag_disabled
:
Boolean
(
item_user_state
||
state
.
finished
),
class_name
:
item_user_state
&&
(
'input'
in
item_user_state
||
item_user_state
.
correct_input
)
?
'fade'
:
undefined
,
class_name
:
placed
||
state
.
finished
?
'fade'
:
undefined
,
xhr_active
:
(
item_user_state
&&
item_user_state
.
submitting_location
),
input
:
input
,
displayName
:
item
.
displayName
,
...
...
@@ -345,6 +530,7 @@ function DragAndDropBlock(runtime, element, configuration) {
};
if
(
item_user_state
)
{
itemProperties
.
is_placed
=
true
;
itemProperties
.
zone
=
item_user_state
.
zone
;
itemProperties
.
x_percent
=
item_user_state
.
x_percent
;
itemProperties
.
y_percent
=
item_user_state
.
y_percent
;
}
...
...
drag_and_drop_v2/public/js/view.js
View file @
8be8937b
...
...
@@ -53,9 +53,18 @@
};
var
itemTemplate
=
function
(
item
)
{
var
style
=
{};
// Define properties
var
className
=
(
item
.
class_name
)
?
item
.
class_name
:
""
;
var
tabindex
=
0
;
if
(
item
.
has_image
)
{
className
+=
" "
+
"option-with-image"
;
}
var
attributes
=
{
'draggable'
:
!
item
.
drag_disabled
,
'aria-grabbed'
:
item
.
grabbed
,
'data-value'
:
item
.
value
,
'data-drag-disabled'
:
item
.
drag_disabled
};
var
style
=
{};
if
(
item
.
background_color
)
{
style
[
'background-color'
]
=
item
.
background_color
;
}
...
...
@@ -68,34 +77,42 @@
if
(
item
.
is_placed
)
{
style
.
left
=
item
.
x_percent
+
"%"
;
style
.
top
=
item
.
y_percent
+
"%"
;
tabindex
=
-
1
;
// If an item has been placed it can no longer be interacted with,
// so remove the ability to move focus to it using the keyboard
}
if
(
item
.
has_image
)
{
className
+=
" "
+
"option-with-image"
;
}
else
{
// If an item has not been placed it must be possible to move focus to it using the keyboard:
attributes
.
tabindex
=
0
;
}
var
content_html
=
item
.
displayName
;
// Define children
var
children
=
[
itemSpinnerTemplate
(
item
.
xhr_active
),
itemInputTemplate
(
item
.
input
)
];
var
item_content_html
=
item
.
displayName
;
if
(
item
.
imageURL
)
{
content_html
=
'<img src="'
+
item
.
imageURL
+
'" alt="'
+
item
.
imageDescription
+
'" />'
;
item_content_html
=
'<img src="'
+
item
.
imageURL
+
'" alt="'
+
item
.
imageDescription
+
'" />'
;
}
var
item_content
=
h
(
'div'
,
{
innerHTML
:
item_content_html
,
className
:
"item-content"
});
if
(
item
.
is_placed
)
{
// Insert information about zone in which this item has been placed
var
item_description_id
=
'item-'
+
item
.
value
+
'-description'
;
item_content
.
properties
.
attributes
=
{
'aria-describedby'
:
item_description_id
};
var
item_description
=
h
(
'div'
,
{
id
:
item_description_id
,
className
:
'sr'
},
gettext
(
'Correctly placed in: '
)
+
item
.
zone
);
children
.
splice
(
1
,
0
,
item_description
);
}
children
.
splice
(
1
,
0
,
item_content
);
return
(
h
(
'div.option'
,
h
(
'div.option'
,
{
key
:
item
.
value
,
className
:
className
,
attributes
:
{
'tabindex'
:
tabindex
,
'draggable'
:
!
item
.
drag_disabled
,
'aria-grabbed'
:
item
.
grabbed
,
'data-value'
:
item
.
value
,
'data-drag-disabled'
:
item
.
drag_disabled
},
attributes
:
attributes
,
style
:
style
},
[
itemSpinnerTemplate
(
item
.
xhr_active
),
h
(
'div'
,
{
innerHTML
:
content_html
,
className
:
"item-content"
}),
itemInputTemplate
(
item
.
input
)
]
},
children
)
);
};
...
...
@@ -132,10 +149,44 @@
var
properties
=
{
attributes
:
{
'aria-live'
:
'polite'
}
};
return
(
h
(
'section.feedback'
,
properties
,
[
h
(
'div.reset-button'
,
{
style
:
{
display
:
reset_button_display
}},
gettext
(
'Reset exercise'
)),
h
(
'h3.title1'
,
{
style
:
{
display
:
feedback_display
}},
gettext
(
'Feedback'
)),
h
(
'p.message'
,
{
style
:
{
display
:
feedback_display
},
innerHTML
:
ctx
.
feedback_html
})
h
(
'a.reset-button'
,
{
style
:
{
display
:
reset_button_display
},
attributes
:
{
tabindex
:
0
}},
gettext
(
'Reset exercise'
)
),
h
(
'h3.title1'
,
{
style
:
{
display
:
feedback_display
}
},
gettext
(
'Feedback'
)),
h
(
'p.message'
,
{
style
:
{
display
:
feedback_display
},
innerHTML
:
ctx
.
feedback_html
})
])
);
};
var
keyboardHelpTemplate
=
function
(
ctx
)
{
var
dialog_attributes
=
{
role
:
'dialog'
,
'aria-labelledby'
:
'modal-window-title'
};
var
dialog_style
=
{};
return
(
h
(
'section.keyboard-help'
,
[
h
(
'a.keyboard-help-button'
,
{
attributes
:
{
tabindex
:
0
}
},
gettext
(
'Keyboard Help'
)),
h
(
'div.keyboard-help-dialog'
,
[
h
(
'div.modal-window-overlay'
),
h
(
'div.modal-window'
,
{
attributes
:
dialog_attributes
,
style
:
dialog_style
},
[
h
(
'div.modal-header'
,
[
h
(
'h2.modal-window-title'
,
gettext
(
'Keyboard Help'
))
]),
h
(
'div.modal-content'
,
[
h
(
'p'
,
gettext
(
'You can complete this exercise using only your keyboard.'
)),
h
(
'ul'
,
[
h
(
'li'
,
gettext
(
'Use "Tab" and "Shift-Tab" to navigate between items and zones.'
)),
h
(
'li'
,
gettext
(
'Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, then navigate to the zone you want to drop it on.'
)),
h
(
'li'
,
gettext
(
'Press "Enter", "Space", "Ctrl-m", or "⌘-m" to drop the item on the current zone.'
)),
h
(
'li'
,
gettext
(
'Press "Escape" if you want to cancel the drop operation (e.g. because you would like to select a different item).'
)),
h
(
'li'
,
gettext
(
'Press "?" at any time to bring up this dialog.'
)),
])
]),
h
(
'div.modal-actions'
,
[
h
(
'button.modal-dismiss-button'
,
gettext
(
"OK"
))
])
])
])
])
);
};
...
...
@@ -156,10 +207,17 @@
h
(
'section.drag-container'
,
[
h
(
'div.item-bank'
,
renderCollection
(
itemTemplate
,
items_in_bank
,
ctx
)),
h
(
'div.target'
,
[
h
(
'div.popup'
,
{
style
:
{
display
:
ctx
.
popup_html
?
'block'
:
'none'
}},
[
h
(
'div.popup'
,
{
style
:
{
display
:
ctx
.
popup_html
?
'block'
:
'none'
},
attributes
:
{
'aria-live'
:
'polite'
}
},
[
h
(
'div.close.icon-remove-sign.fa-times-circle'
),
h
(
'p.popup-content'
,
{
innerHTML
:
ctx
.
popup_html
}),
]),
]
),
h
(
'div.target-img-wrapper'
,
[
h
(
'img.target-img'
,
{
src
:
ctx
.
target_img_src
,
alt
:
ctx
.
target_img_description
}),
]),
...
...
@@ -167,6 +225,7 @@
renderCollection
(
itemTemplate
,
items_placed
,
ctx
),
]),
]),
keyboardHelpTemplate
(
ctx
),
feedbackTemplate
(
ctx
),
])
);
...
...
tests/integration/test_base.py
View file @
8be8937b
...
...
@@ -57,6 +57,24 @@ class BaseIntegrationTest(SeleniumBaseTest):
def
_get_zones
(
self
):
return
self
.
_page
.
find_elements_by_css_selector
(
".drag-container .zone"
)
def
_get_popup
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".popup"
)
def
_get_popup_content
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".popup .popup-content"
)
def
_get_keyboard_help
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".keyboard-help"
)
def
_get_keyboard_help_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".keyboard-help .keyboard-help-button"
)
def
_get_keyboard_help_dialog
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".keyboard-help .keyboard-help-dialog"
)
def
_get_reset_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.reset-button'
)
def
_get_feedback
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".feedback"
)
...
...
tests/integration/test_interaction.py
View file @
8be8937b
from
ddt
import
ddt
,
data
from
selenium.common.exceptions
import
NoSuchElementException
from
selenium.webdriver
import
ActionChains
from
selenium.webdriver.common.keys
import
Keys
from
drag_and_drop_v2.default_data
import
START_FEEDBACK
,
FINISH_FEEDBACK
from
drag_and_drop_v2.default_data
import
(
TOP_ZONE_TITLE
,
MIDDLE_ZONE_TITLE
,
BOTTOM_ZONE_TITLE
,
ITEM_CORRECT_FEEDBACK
,
ITEM_INCORRECT_FEEDBACK
,
ITEM_NO_ZONE_FEEDBACK
,
START_FEEDBACK
,
FINISH_FEEDBACK
)
from
.test_base
import
BaseIntegrationTest
from
..utils
import
load_resource
ZONES_MAP
=
{
0
:
TOP_ZONE_TITLE
,
1
:
MIDDLE_ZONE_TITLE
,
2
:
BOTTOM_ZONE_TITLE
,
}
class
ItemDefinition
(
object
):
def
__init__
(
self
,
item_id
,
zone_id
,
feedback_positive
,
feedback_negative
,
input_value
=
None
):
self
.
feedback_negative
=
feedback_negative
...
...
@@ -16,12 +31,19 @@ class ItemDefinition(object):
class
InteractionTestBase
(
object
):
@classmethod
def
_get_
correct_item_for
_zone
(
cls
,
items_map
):
def
_get_
items_with
_zone
(
cls
,
items_map
):
return
{
item_key
:
definition
for
item_key
,
definition
in
items_map
.
items
()
if
definition
.
zone_id
is
not
None
}
@classmethod
def
_get_items_without_zone
(
cls
,
items_map
):
return
{
item_key
:
definition
for
item_key
,
definition
in
items_map
.
items
()
if
definition
.
zone_id
is
None
}
def
setUp
(
self
):
super
(
InteractionTestBase
,
self
)
.
setUp
()
...
...
@@ -37,6 +59,10 @@ class InteractionTestBase(object):
items_container
=
self
.
_page
.
find_element_by_css_selector
(
'.item-bank'
)
return
items_container
.
find_elements_by_xpath
(
"//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
def
_get_placed_item_by_value
(
self
,
item_value
):
items_container
=
self
.
_page
.
find_element_by_css_selector
(
'.target'
)
return
items_container
.
find_elements_by_xpath
(
"//div[@data-value='{item_id}']"
.
format
(
item_id
=
item_value
))[
0
]
def
_get_zone_by_id
(
self
,
zone_id
):
zones_container
=
self
.
_page
.
find_element_by_css_selector
(
'.target'
)
return
zones_container
.
find_elements_by_xpath
(
"//div[@data-zone='{zone_id}']"
.
format
(
zone_id
=
zone_id
))[
0
]
...
...
@@ -45,17 +71,19 @@ class InteractionTestBase(object):
element
=
self
.
_get_item_by_value
(
item_value
)
return
element
.
find_element_by_class_name
(
'numerical-input'
)
def
_send_input
(
self
,
item_value
,
value
):
element
=
self
.
_get_item_by_value
(
item_value
)
self
.
wait_until_visible
(
element
)
element
.
find_element_by_class_name
(
'input'
)
.
send_keys
(
value
)
element
.
find_element_by_class_name
(
'submit-input'
)
.
click
()
def
_get_zone_position
(
self
,
zone_id
):
return
self
.
browser
.
execute_script
(
'return $("div[data-zone=
\'
{zone_id}
\'
]").prevAll(".zone").length'
.
format
(
zone_id
=
zone_id
)
)
def
get_feedback_popup
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
".popup-content"
)
def
_focus_item
(
self
,
item_position
):
self
.
browser
.
execute_script
(
"$('.option:nth-child({n})').focus()"
.
format
(
n
=
item_position
+
1
)
)
def
get_reset_button
(
self
):
return
self
.
_page
.
find_element_by_css_selector
(
'.reset-button'
)
def
place_item
(
self
,
item_value
,
zone_id
,
action_key
=
None
):
if
action_key
is
None
:
self
.
drag_item_to_zone
(
item_value
,
zone_id
)
else
:
self
.
move_item_to_zone
(
item_value
,
zone_id
,
action_key
)
def
drag_item_to_zone
(
self
,
item_value
,
zone_id
):
element
=
self
.
_get_item_by_value
(
item_value
)
...
...
@@ -63,29 +91,99 @@ class InteractionTestBase(object):
action_chains
=
ActionChains
(
self
.
browser
)
action_chains
.
drag_and_drop
(
element
,
target
)
.
perform
()
def
parameterized_item_positive_feedback_on_good_move
(
self
,
items_map
,
scroll_down
=
100
):
def
move_item_to_zone
(
self
,
item_value
,
zone_id
,
action_key
):
# Get item position
item_position
=
item_value
# Get zone position
zone_position
=
self
.
_get_zone_position
(
zone_id
)
self
.
_focus_item
(
0
)
focused_item
=
self
.
_get_item_by_value
(
0
)
for
i
in
range
(
item_position
):
focused_item
.
send_keys
(
Keys
.
TAB
)
focused_item
=
self
.
_get_item_by_value
(
i
+
1
)
focused_item
.
send_keys
(
action_key
)
# Focus is on first *zone* now
self
.
assert_grabbed_item
(
focused_item
)
focused_zone
=
self
.
_get_zone_by_id
(
ZONES_MAP
[
0
])
for
i
in
range
(
zone_position
):
focused_zone
.
send_keys
(
Keys
.
TAB
)
focused_zone
=
self
.
_get_zone_by_id
(
ZONES_MAP
[
i
+
1
])
focused_zone
.
send_keys
(
action_key
)
def
send_input
(
self
,
item_value
,
value
):
element
=
self
.
_get_item_by_value
(
item_value
)
self
.
wait_until_visible
(
element
)
element
.
find_element_by_class_name
(
'input'
)
.
send_keys
(
value
)
element
.
find_element_by_class_name
(
'submit-input'
)
.
click
()
def
assert_grabbed_item
(
self
,
item
):
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'true'
)
def
assert_placed_item
(
self
,
item_value
,
zone_id
):
item
=
self
.
_get_placed_item_by_value
(
item_value
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
item_description
=
item
.
find_element_by_css_selector
(
'.sr'
)
item_description_id
=
'item-{}-description'
.
format
(
item_value
)
self
.
assertIsNone
(
item
.
get_attribute
(
'tabindex'
))
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'true'
)
self
.
assertEqual
(
item_content
.
get_attribute
(
'aria-describedby'
),
item_description_id
)
self
.
assertEqual
(
item_description
.
get_attribute
(
'id'
),
item_description_id
)
self
.
assertEqual
(
item_description
.
text
,
'Correctly placed in: {}'
.
format
(
zone_id
))
def
assert_reverted_item
(
self
,
item_value
):
item
=
self
.
_get_item_by_value
(
item_value
)
item_content
=
item
.
find_element_by_css_selector
(
'.item-content'
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option ui-draggable'
)
self
.
assertEqual
(
item
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertEqual
(
item
.
get_attribute
(
'draggable'
),
'true'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'false'
)
self
.
assertIsNone
(
item_content
.
get_attribute
(
'aria-describedby'
))
try
:
item
.
find_element_by_css_selector
(
'.sr'
)
except
NoSuchElementException
:
pass
else
:
self
.
fail
(
'Reverted item should not have .sr description.'
)
def
assert_decoy_items
(
self
,
items_map
):
decoy_items
=
self
.
_get_items_without_zone
(
items_map
)
for
item_key
in
decoy_items
:
item
=
self
.
_get_item_by_value
(
item_key
)
self
.
assertEqual
(
item
.
get_attribute
(
'class'
),
'option fade'
)
self
.
assertEqual
(
item
.
get_attribute
(
'aria-grabbed'
),
'false'
)
self
.
assertEqual
(
item
.
get_attribute
(
'data-drag-disabled'
),
'true'
)
def
parameterized_item_positive_feedback_on_good_move
(
self
,
items_map
,
scroll_down
=
100
,
action_key
=
None
):
# Scroll drop zones into view to make sure Selenium can successfully drop items
self
.
scroll_down
(
pixels
=
scroll_down
)
for
definition
in
self
.
_get_
correct_item_for
_zone
(
items_map
)
.
values
():
for
definition
in
self
.
_get_
items_with
_zone
(
items_map
)
.
values
():
if
not
definition
.
input
:
self
.
drag_item_to_zone
(
definition
.
item_id
,
definition
.
zone_id
)
feedback_popup
=
self
.
get_feedback_popup
()
self
.
wait_until_html_in
(
definition
.
feedback_positive
,
feedback_popup
)
self
.
place_item
(
definition
.
item_id
,
definition
.
zone_id
,
action_key
)
feedback_popup_content
=
self
.
_get_popup_content
()
self
.
wait_until_html_in
(
definition
.
feedback_positive
,
feedback_popup_content
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_id
)
def
parameterized_item_positive_feedback_on_good_input
(
self
,
items_map
,
scroll_down
=
100
):
def
parameterized_item_positive_feedback_on_good_input
(
self
,
items_map
,
scroll_down
=
100
,
action_key
=
None
):
self
.
scroll_down
(
pixels
=
scroll_down
)
feedback_popup
=
self
.
get_feedback_popup
()
for
definition
in
self
.
_get_
correct_item_for
_zone
(
items_map
)
.
values
():
feedback_popup
_content
=
self
.
_get_popup_content
()
for
definition
in
self
.
_get_
items_with
_zone
(
items_map
)
.
values
():
if
definition
.
input
:
self
.
drag_item_to_zone
(
definition
.
item_id
,
definition
.
zone_id
)
self
.
_
send_input
(
definition
.
item_id
,
definition
.
input
)
self
.
place_item
(
definition
.
item_id
,
definition
.
zone_id
,
action_key
)
self
.
send_input
(
definition
.
item_id
,
definition
.
input
)
input_div
=
self
.
_get_input_div_by_value
(
definition
.
item_id
)
self
.
wait_until_has_class
(
'correct'
,
input_div
)
self
.
wait_until_html_in
(
definition
.
feedback_positive
,
feedback_popup
)
self
.
wait_until_html_in
(
definition
.
feedback_positive
,
feedback_popup_content
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_id
)
def
parameterized_item_negative_feedback_on_bad_move
(
self
,
items_map
,
all_zones
,
scroll_down
=
100
):
feedback_popup
=
self
.
get_feedback_popup
()
def
parameterized_item_negative_feedback_on_bad_move
(
self
,
items_map
,
all_zones
,
scroll_down
=
100
,
action_key
=
None
):
feedback_popup
_content
=
self
.
_get_popup_content
()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self
.
scroll_down
(
pixels
=
scroll_down
)
...
...
@@ -94,28 +192,30 @@ class InteractionTestBase(object):
for
zone
in
all_zones
:
if
zone
==
definition
.
zone_id
:
continue
self
.
drag_item_to_zone
(
definition
.
item_id
,
zone
)
self
.
wait_until_html_in
(
definition
.
feedback_negative
,
feedback_popup
)
self
.
place_item
(
definition
.
item_id
,
zone
,
action_key
)
self
.
wait_until_html_in
(
definition
.
feedback_negative
,
feedback_popup_content
)
self
.
assert_reverted_item
(
definition
.
item_id
)
def
parameterized_item_
positive_feedback_on_bad_input
(
self
,
items_map
,
scroll_down
=
100
):
feedback_popup
=
self
.
get_feedback_popup
()
def
parameterized_item_
negative_feedback_on_bad_input
(
self
,
items_map
,
scroll_down
=
100
,
action_key
=
None
):
feedback_popup
_content
=
self
.
_get_popup_content
()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self
.
scroll_down
(
pixels
=
scroll_down
)
for
definition
in
self
.
_get_
correct_item_for
_zone
(
items_map
)
.
values
():
for
definition
in
self
.
_get_
items_with
_zone
(
items_map
)
.
values
():
if
definition
.
input
:
self
.
drag_item_to_zone
(
definition
.
item_id
,
definition
.
zone_id
)
self
.
_
send_input
(
definition
.
item_id
,
'1999999'
)
self
.
place_item
(
definition
.
item_id
,
definition
.
zone_id
,
action_key
)
self
.
send_input
(
definition
.
item_id
,
'1999999'
)
input_div
=
self
.
_get_input_div_by_value
(
definition
.
item_id
)
self
.
wait_until_has_class
(
'incorrect'
,
input_div
)
self
.
wait_until_html_in
(
definition
.
feedback_negative
,
feedback_popup
)
self
.
wait_until_html_in
(
definition
.
feedback_negative
,
feedback_popup_content
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_id
)
def
parameterized_final_feedback_and_reset
(
self
,
items_map
,
feedback
,
scroll_down
=
100
):
def
parameterized_final_feedback_and_reset
(
self
,
items_map
,
feedback
,
scroll_down
=
100
,
action_key
=
None
):
feedback_message
=
self
.
_get_feedback_message
()
self
.
assertEqual
(
self
.
get_element_html
(
feedback_message
),
feedback
[
'intro'
])
# precondition check
items
=
self
.
_get_
correct_item_for
_zone
(
items_map
)
items
=
self
.
_get_
items_with
_zone
(
items_map
)
def
get_locations
():
return
{
item_id
:
self
.
_get_item_by_value
(
item_id
)
.
location
for
item_id
in
items
.
keys
()}
...
...
@@ -126,18 +226,25 @@ class InteractionTestBase(object):
self
.
scroll_down
(
pixels
=
scroll_down
)
for
item_key
,
definition
in
items
.
items
():
self
.
drag_item_to_zone
(
item_key
,
definition
.
zone_id
)
self
.
place_item
(
definition
.
item_id
,
definition
.
zone_id
,
action_key
)
if
definition
.
input
:
self
.
_
send_input
(
item_key
,
definition
.
input
)
self
.
send_input
(
item_key
,
definition
.
input
)
input_div
=
self
.
_get_input_div_by_value
(
item_key
)
self
.
wait_until_has_class
(
'correct'
,
input_div
)
self
.
assert_placed_item
(
definition
.
item_id
,
definition
.
zone_id
)
self
.
wait_until_html_in
(
feedback
[
'final'
],
self
.
_get_feedback_message
())
# Check decoy items
self
.
assert_decoy_items
(
items_map
)
# Scroll "Reset exercise" button into view to make sure Selenium can successfully click it
self
.
scroll_down
(
pixels
=
scroll_down
+
150
)
reset
=
self
.
get_reset_button
()
reset
=
self
.
_get_reset_button
()
if
action_key
is
not
None
:
# Using keyboard to interact with block
reset
.
send_keys
(
Keys
.
RETURN
)
else
:
reset
.
click
()
self
.
wait_until_html_in
(
feedback
[
'intro'
],
self
.
_get_feedback_message
())
...
...
@@ -145,22 +252,62 @@ class InteractionTestBase(object):
locations_after_reset
=
get_locations
()
for
item_key
in
items
.
keys
():
self
.
assertDictEqual
(
locations_after_reset
[
item_key
],
initial_locations
[
item_key
])
self
.
assert_reverted_item
(
item_key
)
def
interact_with_keyboard_help
(
self
,
scroll_down
=
250
,
use_keyboard
=
False
):
keyboard_help_button
=
self
.
_get_keyboard_help_button
()
keyboard_help_dialog
=
self
.
_get_keyboard_help_dialog
()
dialog_modal_overlay
=
keyboard_help_dialog
.
find_element_by_css_selector
(
'.modal-window-overlay'
)
dialog_modal
=
keyboard_help_dialog
.
find_element_by_css_selector
(
'.modal-window'
)
dialog_dismiss_button
=
dialog_modal
.
find_element_by_css_selector
(
'.modal-dismiss-button'
)
# Scroll "Keyboard help" button into view to make sure Selenium can successfully click it
self
.
scroll_down
(
pixels
=
scroll_down
)
if
use_keyboard
:
keyboard_help_button
.
send_keys
(
Keys
.
RETURN
)
else
:
keyboard_help_button
.
click
()
self
.
assertTrue
(
dialog_modal_overlay
.
is_displayed
())
self
.
assertTrue
(
dialog_modal
.
is_displayed
())
if
use_keyboard
:
dialog_dismiss_button
.
send_keys
(
Keys
.
RETURN
)
else
:
dialog_dismiss_button
.
click
()
self
.
assertFalse
(
dialog_modal_overlay
.
is_displayed
())
self
.
assertFalse
(
dialog_modal
.
is_displayed
())
if
use_keyboard
:
# Try again with "?" key
self
.
_page
.
send_keys
(
"?"
)
self
.
assertTrue
(
dialog_modal_overlay
.
is_displayed
())
self
.
assertTrue
(
dialog_modal
.
is_displayed
())
class
BasicInteractionTest
(
InteractionTestBase
):
"""
Verifying Drag and Drop XBlock rendering against default data - if default data changes this will probably
break.
Testing interactions with Drag and Drop XBlock against default data. If default data changes this will
break.
"""
PAGE_TITLE
=
'Drag and Drop v2'
PAGE_ID
=
'drag_and_drop_v2'
items_map
=
{
0
:
ItemDefinition
(
0
,
'Zone 1'
,
"Yes, it's a 1"
,
"No, 1 does not belong here"
),
1
:
ItemDefinition
(
1
,
'Zone 2'
,
"Yes, it's a 2"
,
"No, 2 does not belong here"
),
2
:
ItemDefinition
(
2
,
None
,
""
,
"You silly, there are no zones for X"
)
0
:
ItemDefinition
(
0
,
TOP_ZONE_TITLE
,
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
TOP_ZONE_TITLE
),
ITEM_INCORRECT_FEEDBACK
),
1
:
ItemDefinition
(
1
,
MIDDLE_ZONE_TITLE
,
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
MIDDLE_ZONE_TITLE
),
ITEM_INCORRECT_FEEDBACK
),
2
:
ItemDefinition
(
2
,
BOTTOM_ZONE_TITLE
,
ITEM_CORRECT_FEEDBACK
.
format
(
zone
=
BOTTOM_ZONE_TITLE
),
ITEM_INCORRECT_FEEDBACK
),
3
:
ItemDefinition
(
3
,
None
,
""
,
ITEM_NO_ZONE_FEEDBACK
),
}
all_zones
=
[
'Zone 1'
,
'Zone 2'
]
all_zones
=
[
TOP_ZONE_TITLE
,
MIDDLE_ZONE_TITLE
,
BOTTOM_ZONE_TITLE
]
feedback
=
{
"intro"
:
START_FEEDBACK
,
...
...
@@ -179,12 +326,41 @@ class BasicInteractionTest(InteractionTestBase):
def
test_item_negative_feedback_on_bad_move
(
self
):
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
)
def
test_item_
posi
tive_feedback_on_bad_input
(
self
):
self
.
parameterized_item_
posi
tive_feedback_on_bad_input
(
self
.
items_map
)
def
test_item_
nega
tive_feedback_on_bad_input
(
self
):
self
.
parameterized_item_
nega
tive_feedback_on_bad_input
(
self
.
items_map
)
def
test_final_feedback_and_reset
(
self
):
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
)
def
test_keyboard_help
(
self
):
self
.
interact_with_keyboard_help
()
@ddt
class
KeyboardInteractionTest
(
BasicInteractionTest
,
BaseIntegrationTest
):
@data
(
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_positive_feedback_on_good_move_with_keyboard
(
self
,
action_key
):
self
.
parameterized_item_positive_feedback_on_good_move
(
self
.
items_map
,
action_key
=
action_key
)
@data
(
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_positive_feedback_on_good_input_with_keyboard
(
self
,
action_key
):
self
.
parameterized_item_positive_feedback_on_good_input
(
self
.
items_map
,
action_key
=
action_key
)
@data
(
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_negative_feedback_on_bad_move_with_keyboard
(
self
,
action_key
):
self
.
parameterized_item_negative_feedback_on_bad_move
(
self
.
items_map
,
self
.
all_zones
,
action_key
=
action_key
)
@data
(
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_item_negative_feedback_on_bad_input_with_keyboard
(
self
,
action_key
):
self
.
parameterized_item_negative_feedback_on_bad_input
(
self
.
items_map
,
action_key
=
action_key
)
@data
(
Keys
.
RETURN
,
Keys
.
SPACE
,
Keys
.
CONTROL
+
'm'
,
Keys
.
COMMAND
+
'm'
)
def
test_final_feedback_and_reset_with_keyboard
(
self
,
action_key
):
self
.
parameterized_final_feedback_and_reset
(
self
.
items_map
,
self
.
feedback
,
action_key
=
action_key
)
def
test_keyboard_help
(
self
):
self
.
interact_with_keyboard_help
(
use_keyboard
=
True
)
class
CustomDataInteractionTest
(
BasicInteractionTest
,
BaseIntegrationTest
):
items_map
=
{
...
...
@@ -284,11 +460,11 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
self
.
item_maps
[
'block2'
],
self
.
all_zones
[
'block2'
],
scroll_down
=
900
)
def
test_item_
posi
tive_feedback_on_bad_input
(
self
):
def
test_item_
nega
tive_feedback_on_bad_input
(
self
):
self
.
_switch_to_block
(
0
)
self
.
parameterized_item_
posi
tive_feedback_on_bad_input
(
self
.
item_maps
[
'block1'
])
self
.
parameterized_item_
nega
tive_feedback_on_bad_input
(
self
.
item_maps
[
'block1'
])
self
.
_switch_to_block
(
1
)
self
.
parameterized_item_
posi
tive_feedback_on_bad_input
(
self
.
item_maps
[
'block2'
],
scroll_down
=
900
)
self
.
parameterized_item_
nega
tive_feedback_on_bad_input
(
self
.
item_maps
[
'block2'
],
scroll_down
=
900
)
def
test_final_feedback_and_reset
(
self
):
self
.
_switch_to_block
(
0
)
...
...
tests/integration/test_render.py
View file @
8be8937b
...
...
@@ -179,6 +179,30 @@ class TestDragAndDropRender(BaseIntegrationTest):
# Zone description should only be visible to screen readers:
self
.
assertEqual
(
zone_description
.
get_attribute
(
'class'
),
'zone-description sr'
)
def
test_popup
(
self
):
self
.
load_scenario
()
popup
=
self
.
_get_popup
()
popup_content
=
self
.
_get_popup_content
()
self
.
assertFalse
(
popup
.
is_displayed
())
self
.
assertEqual
(
popup
.
get_attribute
(
'aria-live'
),
'polite'
)
self
.
assertEqual
(
popup_content
.
text
,
""
)
def
test_keyboard_help
(
self
):
self
.
load_scenario
()
self
.
_get_keyboard_help
()
keyboard_help_button
=
self
.
_get_keyboard_help_button
()
keyboard_help_dialog
=
self
.
_get_keyboard_help_dialog
()
dialog_modal_overlay
=
keyboard_help_dialog
.
find_element_by_css_selector
(
'.modal-window-overlay'
)
dialog_modal
=
keyboard_help_dialog
.
find_element_by_css_selector
(
'.modal-window'
)
self
.
assertEqual
(
keyboard_help_button
.
get_attribute
(
'tabindex'
),
'0'
)
self
.
assertFalse
(
dialog_modal_overlay
.
is_displayed
())
self
.
assertFalse
(
dialog_modal
.
is_displayed
())
self
.
assertEqual
(
dialog_modal
.
get_attribute
(
'role'
),
'dialog'
)
self
.
assertEqual
(
dialog_modal
.
get_attribute
(
'aria-labelledby'
),
'modal-window-title'
)
def
test_feedback
(
self
):
self
.
load_scenario
()
...
...
tests/unit/test_advanced.py
View file @
8be8937b
...
...
@@ -87,6 +87,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
})
def
test_do_attempt_with_input
(
self
):
# Drop item that requires numerical input
data
=
{
"val"
:
1
,
"zone"
:
self
.
ZONE_2
,
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
}
res
=
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
assertEqual
(
res
,
{
...
...
@@ -99,13 +100,16 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state
=
{
'items'
:
{
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"correct_input"
:
False
},
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"correct_input"
:
False
,
"zone"
:
self
.
ZONE_2
,
},
},
'finished'
:
False
,
'overall_feedback'
:
self
.
initial_feedback
(),
}
self
.
assertEqual
(
expected_state
,
self
.
call_handler
(
'get_user_state'
,
method
=
"GET"
))
# Submit incorrect value
data
=
{
"val"
:
1
,
"input"
:
"250"
}
res
=
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
assertEqual
(
res
,
{
...
...
@@ -118,13 +122,17 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state
=
{
'items'
:
{
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"input"
:
"250"
,
"correct_input"
:
False
},
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"correct_input"
:
False
,
"zone"
:
self
.
ZONE_2
,
"input"
:
"250"
,
},
},
'finished'
:
False
,
'overall_feedback'
:
self
.
initial_feedback
(),
}
self
.
assertEqual
(
expected_state
,
self
.
call_handler
(
'get_user_state'
,
method
=
"GET"
))
# Submit correct value
data
=
{
"val"
:
1
,
"input"
:
"103"
}
res
=
self
.
call_handler
(
'do_attempt'
,
data
)
self
.
assertEqual
(
res
,
{
...
...
@@ -137,7 +145,10 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state
=
{
'items'
:
{
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"input"
:
"103"
,
"correct_input"
:
True
},
"1"
:
{
"x_percent"
:
"0
%
"
,
"y_percent"
:
"85
%
"
,
"correct_input"
:
True
,
"zone"
:
self
.
ZONE_2
,
"input"
:
"103"
,
},
},
'finished'
:
False
,
'overall_feedback'
:
self
.
initial_feedback
(),
...
...
@@ -177,7 +188,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state
=
{
"items"
:
{
"0"
:
{
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
,
"correct_input"
:
True
}
"0"
:
{
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
,
"correct_input"
:
True
,
"zone"
:
self
.
ZONE_1
}
},
"finished"
:
False
,
'overall_feedback'
:
self
.
initial_feedback
(),
...
...
@@ -198,8 +209,13 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state
=
{
"items"
:
{
"0"
:
{
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
,
"correct_input"
:
True
},
"1"
:
{
"x_percent"
:
"22
%
"
,
"y_percent"
:
"22
%
"
,
"input"
:
"99"
,
"correct_input"
:
True
}
"0"
:
{
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
,
"correct_input"
:
True
,
"zone"
:
self
.
ZONE_1
,
},
"1"
:
{
"x_percent"
:
"22
%
"
,
"y_percent"
:
"22
%
"
,
"correct_input"
:
True
,
"zone"
:
self
.
ZONE_2
,
"input"
:
"99"
,
}
},
"finished"
:
True
,
'overall_feedback'
:
self
.
FINAL_FEEDBACK
,
...
...
tests/unit/test_basics.py
View file @
8be8937b
import
unittest
from
drag_and_drop_v2.default_data
import
(
TARGET_IMG_DESCRIPTION
,
START_FEEDBACK
,
FINISH_FEEDBACK
,
DEFAULT_DATA
TARGET_IMG_DESCRIPTION
,
TOP_ZONE_TITLE
,
MIDDLE_ZONE_TITLE
,
BOTTOM_ZONE_TITLE
,
START_FEEDBACK
,
FINISH_FEEDBACK
,
DEFAULT_DATA
)
from
..utils
import
make_block
,
TestCaseMixin
...
...
@@ -62,25 +63,25 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
assert_user_state_empty
()
# Drag three items into the correct spot:
data
=
{
"val"
:
0
,
"zone"
:
"The Top Zone"
,
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
}
data
=
{
"val"
:
0
,
"zone"
:
TOP_ZONE_TITLE
,
"x_percent"
:
"33
%
"
,
"y_percent"
:
"11
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
data
=
{
"val"
:
1
,
"zone"
:
"The Middle Zone"
,
"x_percent"
:
"67
%
"
,
"y_percent"
:
"80
%
"
}
data
=
{
"val"
:
1
,
"zone"
:
MIDDLE_ZONE_TITLE
,
"x_percent"
:
"67
%
"
,
"y_percent"
:
"80
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
data
=
{
"val"
:
2
,
"zone"
:
"The Bottom Zone"
,
"x_percent"
:
"99
%
"
,
"y_percent"
:
"95
%
"
}
data
=
{
"val"
:
2
,
"zone"
:
BOTTOM_ZONE_TITLE
,
"x_percent"
:
"99
%
"
,
"y_percent"
:
"95
%
"
}
self
.
call_handler
(
'do_attempt'
,
data
)
# Check the result:
self
.
assertTrue
(
self
.
block
.
completed
)
self
.
assertEqual
(
self
.
block
.
item_state
,
{
'0'
:
{
'x_percent'
:
'33
%
'
,
'y_percent'
:
'11
%
'
},
'1'
:
{
'x_percent'
:
'67
%
'
,
'y_percent'
:
'80
%
'
},
'2'
:
{
'x_percent'
:
'99
%
'
,
'y_percent'
:
'95
%
'
},
'0'
:
{
'x_percent'
:
'33
%
'
,
'y_percent'
:
'11
%
'
,
'zone'
:
TOP_ZONE_TITLE
},
'1'
:
{
'x_percent'
:
'67
%
'
,
'y_percent'
:
'80
%
'
,
'zone'
:
MIDDLE_ZONE_TITLE
},
'2'
:
{
'x_percent'
:
'99
%
'
,
'y_percent'
:
'95
%
'
,
'zone'
:
BOTTOM_ZONE_TITLE
},
})
self
.
assertEqual
(
self
.
call_handler
(
'get_user_state'
),
{
'items'
:
{
'0'
:
{
'x_percent'
:
'33
%
'
,
'y_percent'
:
'11
%
'
,
'correct_input'
:
True
},
'1'
:
{
'x_percent'
:
'67
%
'
,
'y_percent'
:
'80
%
'
,
'correct_input'
:
True
},
'2'
:
{
'x_percent'
:
'99
%
'
,
'y_percent'
:
'95
%
'
,
'correct_input'
:
True
},
'0'
:
{
'x_percent'
:
'33
%
'
,
'y_percent'
:
'11
%
'
,
'correct_input'
:
True
,
'zone'
:
TOP_ZONE_TITLE
},
'1'
:
{
'x_percent'
:
'67
%
'
,
'y_percent'
:
'80
%
'
,
'correct_input'
:
True
,
'zone'
:
MIDDLE_ZONE_TITLE
},
'2'
:
{
'x_percent'
:
'99
%
'
,
'y_percent'
:
'95
%
'
,
'correct_input'
:
True
,
'zone'
:
BOTTOM_ZONE_TITLE
},
},
'finished'
:
True
,
'overall_feedback'
:
FINISH_FEEDBACK
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment