Writing comprehensible tests
animint2 is an R package
for animated, interactive data visualization.
It uses R ggplot code, which makes it possible to define a wide range of web-based data visualizations, as can be seen in the gallery.
The goal of this post is to explain some changes in the framework used for testing animint2
.
Background about animint2
Since animint2
creates web-based data visualizations, we can test
the software by using a web browser to render, and examining the
output HTML. This is a concept called headless
browser testing (if
there is no browser window to be seen), and/or remote-controlled
browser testing (if you control a browser window with the command
line).
Since about 2014, the animint project has been using this kind of
testing, to make sure that the code keeps working, even when making
changes to the code. Currently, these tests are found in the 59
test-renderer*
files in
animint2/tests/testthat,
which contain 755 expect_*
test lines.
Originally, these tests were implemented using RSelenium, a package that supported firefox as a remote-controlled browser, and PhantomJS as a headless browser. Currently in GSOC’24, Siddhesh Deodhar is working in PR#126 on migrating these tests to the chromote package, which is a different implementation that requires the Google Chrome browser.
Since animint2
creates interactive data visualizations, we need to
test the interactivity, by simulating mouse clicks and keyboard typing
in various areas of the web page. The different testing frameworks
(RSelenium and chromote) use different APIs to simulate these
interactivity events. The rest of this post will explore some
challenges we encountered when migrating these interactivity events in
the tests.
Old test
As an example, consider the code below, which tests for the number of circles, before and after sending various key interaction events to the web browser.
test_that("top widget adds/remove points", {
expect_equal(get_circles(), list(10, 10))
remDr$sendKeysToActiveElement(list(key="backspace"))
expect_equal(get_circles(), list(5, 10))
remDr$sendKeysToActiveElement(list(key="backspace"))
expect_equal(get_circles(), list(0, 10))
remDr$sendKeysToActiveElement(list("a", key="enter"))
expect_equal(get_circles(), list(5, 10))
remDr$sendKeysToActiveElement(list("b", key="enter"))
expect_equal(get_circles(), list(10, 10))
})
The code above is fairly easy to read/understand:
get_circles()
gets a list of counts of currently displayed circles.remDr$sendKeysToActiveElement()
simulates a keyboard interaction event, whereenter
andbackspace
are special keywords for the corresponding keys.
Siddesh’s first proposition
In chromote, the analog of sendKeysToActiveElement()
is
dispatchKeyEvent()
, which requires specifying a key and
corresponding code, as in the example below, from one of Siddhesh’s
commits:
sendKey <- function(key, code, keyCode) {
remDr$Input$dispatchKeyEvent(
type = "keyDown", key = key, code = code,
windowsVirtualKeyCode = keyCode,
nativeVirtualKeyCode = keyCode)
remDr$Input$dispatchKeyEvent(
type = "keyUp", key = key, code = code,
windowsVirtualKeyCode = keyCode,
nativeVirtualKeyCode = keyCode)
}
sendBackspace <- function() {
if (remDr$browserName == "chromote") {
sendKey("Backspace", "Backspace", 8)
} else {
remDr$sendKeysToActiveElement(list(key="backspace"))
}
Sys.sleep(0.5)
}
sendA <- function() {
if (remDr$browserName == "chromote") {
remDr$Input$insertText(text = "a")
sendKey("Enter", "Enter", 13)
} else {
remDr$sendKeysToActiveElement(list("a", key="enter"))
}
Sys.sleep(0.5)
}
sendB <- function() {
if (remDr$browserName == "chromote") {
remDr$Input$insertText(text = "b")
sendKey("Enter", "Enter", 13)
} else {
remDr$sendKeysToActiveElement(list("b", key="enter"))
}
Sys.sleep(0.5)
}
There are several instances of “repeated code blocks” in the code above, which is one of the mistakes that I see frequently enough, that I have written about it in my R General Usage Rubric, which I use to grade R programming assignments. The repetition issues below can be fixed by using loops, functions, or some other programming technique:
- In
sendKey()
the two lines are the same except fortype = "keyUp"
orDown
. - The
sendA
andsendB
functions are identical except fortext="a"
orb
. sendKey(x,y,z)
is always called withx
same asy
, such assendKey("Enter", "Enter", 13)
.
Another issue is the 13
which appears in the code, but is not
explained, so it is a Magic
Number. Any
reasonable reader of the code may wonder, where does this 13 come
from, and what number should I use if I wanted to call this function
to simulate a different key press?
Siddesh’s proposed fix
I asked Siddhesh to fix these issues, and he commited a new version with:
sendKey <- function(key) {
stopifnot(is.character(key))
#The key codes in the list below are adopted from Windows Virtual keycode standards
#https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
# VK_BACK->Backspace, VK_RETURN->Enter, VK_DOWN->ArrowDown
# we use the corresponding decimal values of the key codes given in hex value in the above link
key2code <- c(
Backspace=8,
Enter=13,
ArrowDown=40)
for (type in c("keyDown", "keyUp")) {
remDr$Input$dispatchKeyEvent(
type = type, key = key, code = key,
windowsVirtualKeyCode = key2code[[key]],
nativeVirtualKeyCode = key2code[[key]])
}
}
sendBackspace <- function() {
sendKey("Backspace")
Sys.sleep(0.5)
}
send <- function(alphabet) {
remDr$Input$insertText(text = alphabet)
sendKey("Enter")
Sys.sleep(0.5)
}
test_that("top widget adds/remove points", {
expect_equal(get_circles(), list(10, 10))
sendBackspace()
expect_equal(get_circles(), list(5, 10))
sendBackspace()
expect_equal(get_circles(), list(0, 10))
send("a")
expect_equal(get_circles(), list(5, 10))
send("b")
expect_equal(get_circles(), list(10, 10))
})
The above code fixes most of the repetition issues, but does not fully fix the magic number issue, because the link in comments, to Microsoft docs,
- only has hex codes, not decimal codes (required by chromote in R)
- only has names like
VK_BACK
, not likeBackspace
(required by chromote in R)
So there is still some mystery. Any reasonable programmer may wonder, are these really the decimal versions of the hex codes listed on that page? How to find what the keyword should be for a new key?
My proposed new test
I proposed the following fix in a recent commit:
### The hex codes come from
### https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
key2hex_code <- c(
Backspace="08",
Enter="0D",
ArrowDown="28")
### https://chromedevtools.github.io/devtools-protocol/tot/Input/ says
### that dispatchKeyEvent() requires DOM key codes (in decimal) for
### the windowsVirtualKeyCode and nativeVirtualKeyCode arguments.
key2dec_code <- structure(
strtoi(key2hex_code,base=16),
names=names(key2hex_code))
# Function to send a key event
sendKey <- function(key) {
stopifnot(is.character(key))
for (type in c("keyDown", "keyUp")) {
remDr$Input$dispatchKeyEvent(type = type, key = key, code = key, windowsVirtualKeyCode = key2dec_code[[key]], nativeVirtualKeyCode = key2dec_code[[key]])
}
}
The above code provides two links
- Chrome dev tools docs explain that
dispatchKeyEvent()
requires DOM key codes. - Mozilla
docs
has a table that lists DOM key names and corresponding codes like
Backspace
(along with the windows virtual key codes likeVK_BACK
).
So by following those links in the nearby comments, it should be obvious, to find the key codes to use, if we ever wanted to simulate a new/different key entry. And making such a change would be easy, by just adding a new entry to key2hex_code
, since the repetition has been removed.
What about the other hex codes?
Another reasonable question we may ask is, can our R code support all of the key codes listed on Mozilla docs?
To do that, we could download the web page, and parse/extract the relevant information (names and hex code values).
Define expected values
First, we define what we expect to extract from that web page:
library(data.table)
## data.table 1.15.99 IN DEVELOPMENT built 2024-05-02 04:20:26 UTC using 1 threads (see ?getDTthreads). Latest news: r-datatable.com
## **********
## This development version of data.table was built more than 4 weeks ago. Please update: data.table::update_dev_pkg()
## **********
code <- function(dom_name, key_name, hex){
data.table(dom_name, key_name, hex)
}
(animint2.expected <- rbind(
code("Backspace", "VK_BACK", "08"),
code("Enter", "VK_RETURN", "0D"),
code("ArrowDown", "VK_DOWN", "28")))
## dom_name key_name hex
## <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Enter VK_RETURN 0D
## 3: ArrowDown VK_DOWN 28
The table above has three rows, one for each key code that we used in
the animint2
test code.
Create input text for a small, relevant example
When extracting data from loosely structured text in web pages, the first thing to do is download a copy of the web page,
local.html <- "~/teaching/regex-tutorial/Keyboard_event_key_values.html"
if(!file.exists(local.html)){
remote.url <- "https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values"
download.file(remote.url, local.html)
}
html.lines <- readLines(local.html)
## Warning in readLines(local.html): ligne finale incomplète trouvée dans
## '~/teaching/regex-tutorial/Keyboard_event_key_values.html'
Then, instead of trying to extract from the whole web page right away, first open up the local web page in a text editor like emacs, and copy a few relevant parts of the text data to a R string literal. Working with this small example will enable easy/fast testing of the extraction code, because you should know what the correct extraction should look like. For example, below we define a string literal containing five examples from that web page:
some.html <- r'{
<tr>
<td><code>"Backspace"</code></td>
<td>
The <kbd>Backspace</kbd> key. This key is labeled <kbd>Delete</kbd> on
Mac keyboards.
</td>
<td><code>VK_BACK</code> (0x08)</td>
<td><code>kVK_Delete</code> (0x33)</td>
<td>
<code>GDK_KEY_BackSpace</code> (0xFF08)<br><code>Qt::Key_Backspace</code>
(0x01000003)
</td>
<td><code>KEYCODE_DEL</code> (67)</td>
</tr>
<tr>
<td><code>"Enter"</code></td>
<td>
The <kbd>Enter</kbd> or <kbd>↵</kbd> key (sometimes labeled
<kbd>Return</kbd>).
</td>
<td><code>VK_RETURN</code> (0x0D)</td>
<td>
<code>kVK_Return</code> (0x24)<br><code>kVK_ANSI_KeypadEnter</code>
(0x4C)<br><code>kVK_Powerbook_KeypadEnter</code> (0x34)
</td>
<td>
<code>GDK_KEY_Return</code> (0xFF0D)<br><code>GDK_KEY_KP_Enter</code>
(0xFF8D)<br><code>GDK_KEY_ISO_Enter</code> (0xFE34)<br><code>GDK_KEY_3270_Enter</code>
(0xFD1E)<br><code>Qt::Key_Return</code> (0x01000004)<br><code>Qt::Key_Enter</code>
(0x01000005)
</td>
<td>
<code>KEYCODE_ENTER</code> (66)<br><code>KEYCODE_NUMPAD_ENTER</code>
(160)<br><code>KEYCODE_DPAD_CENTER</code> (23)
</td>
</tr>
<tr>
<td><code>"ArrowDown"</code> [1]</td>
<td>The down arrow key.</td>
<td><code>VK_DOWN</code> (0x28)</td>
<td><code>kVK_DownArrow</code> (0x7D)</td>
<td>
<code>GDK_KEY_Down</code> (0xFF54)<br><code>GDK_KEY_KP_Down</code>
(0xFF99)<br><code>Qt::Key_Down</code> (0x01000015)
</td>
<td><code>KEYCODE_DPAD_DOWN</code> (20)</td>
</tr>
<tr>
<td><code>"Alt"</code> [4]</td>
<td>The <kbd>Alt</kbd> (Alternative) key.</td>
<td>
<code>VK_MENU</code> (0x12)<br><code>VK_LMENU</code> (0xA4)<br><code>VK_RMENU</code>
(0xA5)
</td>
<td><code>kVK_Option</code> (0x3A)<br><code>kVK_RightOption</code> (0x3D)</td>
<td>
<code>GDK_KEY_Alt_L</code> (0xFFE9)<br><code>GDK_KEY_Alt_R</code>
(0xFFEA)<br><code>Qt::Key_Alt</code> (0x01000023)
</td>
<td>
<code>KEYCODE_ALT_LEFT</code> (57)<br><code>KEYCODE_ALT_RIGHT</code>
(58)
</td>
</tr>
<tr>
<td><code>"Dead"</code></td>
<td>
<p>
A dead "combining" key; that is, a key which is used in tandem with
other keys to generate accented and other modified characters. If
pressed by itself, it doesn't generate a character.
</p>
<p>
If you wish to identify which specific dead key was pressed (in cases
where more than one exists), you can do so by examining the
<a href="/en-US/docs/Web/API/KeyboardEvent"><code>KeyboardEvent</code></a>'s associated
<a href="/en-US/docs/Web/API/Element/compositionupdate_event" title="compositionupdate"><code>compositionupdate</code></a> event's
<a href="/en-US/docs/Web/API/CompositionEvent/data" title="data"><code>data</code></a> property.
</p>
</td>
<td></td>
<td></td>
<td>See <a href="#dead_keycodes_for_linux">Dead keycodes for Linux</a> below</td>
<td></td>
</tr>
}'
The examples above include the three keys used in animint2
tests
(Backspace, Enter, ArrowDown), as well as one Dead
entry that does
not have any corresponding hex codes (dead
keys)., and one Alt
entry that has more than one hex code.
To extract each DOM name from those text data, we can use my nc
package, as in the code below:
dom_name_pattern <- list(
'<td><code>"',
dom_name='[^"]+')
nc::capture_all_str(some.html, dom_name_pattern)
## dom_name
## <char>
## 1: Backspace
## 2: Enter
## 3: ArrowDown
## 4: Alt
## 5: Dead
The output above is a table with five rows, one for each of the five expected entries. Next, we try to match the rest of the entry after the DOM name,
dom_name_rest_pattern <- list(
dom_name_pattern,
rest="(?:.*\n)*?",
" </tr>")
(dom.rest.dt <- nc::capture_all_str(some.html, dom_name_rest_pattern))
## dom_name
## <char>
## 1: Backspace
## 2: Enter
## 3: ArrowDown
## 4: Alt
## 5: Dead
## rest
## <char>
## 1: "</code></td>\n <td>\n The <kbd>Backspace</kbd> key. This key is labeled <kbd>Delete</kbd> on\n Mac keyboards.\n </td>\n <td><code>VK_BACK</code> (0x08)</td>\n <td><code>kVK_Delete</code> (0x33)</td>\n <td>\n <code>GDK_KEY_BackSpace</code> (0xFF08)<br><code>Qt::Key_Backspace</code>\n (0x01000003)\n </td>\n <td><code>KEYCODE_DEL</code> (67)</td>\n
## 2: "</code></td>\n <td>\n The <kbd>Enter</kbd> or <kbd>↵</kbd> key (sometimes labeled\n <kbd>Return</kbd>).\n </td>\n <td><code>VK_RETURN</code> (0x0D)</td>\n <td>\n <code>kVK_Return</code> (0x24)<br><code>kVK_ANSI_KeypadEnter</code>\n (0x4C)<br><code>kVK_Powerbook_KeypadEnter</code> (0x34)\n </td>\n <td>\n <code>GDK_KEY_Return</code> (0xFF0D)<br><code>GDK_KEY_KP_Enter</code>\n (0xFF8D)<br><code>GDK_KEY_ISO_Enter</code> (0xFE34)<br><code>GDK_KEY_3270_Enter</code>\n (0xFD1E)<br><code>Qt::Key_Return</code> (0x01000004)<br><code>Qt::Key_Enter</code>\n (0x01000005)\n </td>\n <td>\n <code>KEYCODE_ENTER</code> (66)<br><code>KEYCODE_NUMPAD_ENTER</code>\n (160)<br><code>KEYCODE_DPAD_CENTER</code> (23)\n </td>\n
## 3: "</code> [1]</td>\n <td>The down arrow key.</td>\n <td><code>VK_DOWN</code> (0x28)</td>\n <td><code>kVK_DownArrow</code> (0x7D)</td>\n <td>\n <code>GDK_KEY_Down</code> (0xFF54)<br><code>GDK_KEY_KP_Down</code>\n (0xFF99)<br><code>Qt::Key_Down</code> (0x01000015)\n </td>\n <td><code>KEYCODE_DPAD_DOWN</code> (20)</td>\n
## 4: "</code> [4]</td>\n <td>The <kbd>Alt</kbd> (Alternative) key.</td>\n <td>\n <code>VK_MENU</code> (0x12)<br><code>VK_LMENU</code> (0xA4)<br><code>VK_RMENU</code>\n (0xA5)\n </td>\n <td><code>kVK_Option</code> (0x3A)<br><code>kVK_RightOption</code> (0x3D)</td>\n <td>\n <code>GDK_KEY_Alt_L</code> (0xFFE9)<br><code>GDK_KEY_Alt_R</code>\n (0xFFEA)<br><code>Qt::Key_Alt</code> (0x01000023)\n </td>\n <td>\n <code>KEYCODE_ALT_LEFT</code> (57)<br><code>KEYCODE_ALT_RIGHT</code>\n (58)\n </td>\n
## 5: "</code></td>\n <td>\n <p>\n A dead "combining" key; that is, a key which is used in tandem with\n other keys to generate accented and other modified characters. If\n pressed by itself, it doesn't generate a character.\n </p>\n <p>\n If you wish to identify which specific dead key was pressed (in cases\n where more than one exists), you can do so by examining the\n <a href="/en-US/docs/Web/API/KeyboardEvent"><code>KeyboardEvent</code></a>'s associated\n <a href="/en-US/docs/Web/API/Element/compositionupdate_event" title="compositionupdate"><code>compositionupdate</code></a> event's\n <a href="/en-US/docs/Web/API/CompositionEvent/data" title="data"><code>data</code></a> property.\n </p>\n </td>\n <td></td>\n <td></td>\n <td>See <a href="#dead_keycodes_for_linux">Dead keycodes for Linux</a> below</td>\n <td></td>\n
The table above contains a lot of text, but since there are only five rows, it is actually manageable to look through. Next, we use another regex to extract hex codes,
key_name_hex_pattern <- list(
'<code>',
key_name=".*?",
'</code>',
space='\\s+',
'[(]0x',
hex='.*?',
'[)]')
nc::capture_all_str(some.html, key_name_hex_pattern)
## key_name space hex
## <char> <char> <char>
## 1: VK_BACK 08
## 2: kVK_Delete 33
## 3: GDK_KEY_BackSpace FF08
## 4: Qt::Key_Backspace \n 01000003
## 5: VK_RETURN 0D
## 6: kVK_Return 24
## 7: kVK_ANSI_KeypadEnter \n 4C
## 8: kVK_Powerbook_KeypadEnter 34
## 9: GDK_KEY_Return FF0D
## 10: GDK_KEY_KP_Enter \n FF8D
## 11: GDK_KEY_ISO_Enter FE34
## 12: GDK_KEY_3270_Enter \n FD1E
## 13: Qt::Key_Return 01000004
## 14: Qt::Key_Enter \n 01000005
## 15: VK_DOWN 28
## 16: kVK_DownArrow 7D
## 17: GDK_KEY_Down FF54
## 18: GDK_KEY_KP_Down \n FF99
## 19: Qt::Key_Down 01000015
## 20: VK_MENU 12
## 21: VK_LMENU A4
## 22: VK_RMENU \n A5
## 23: kVK_Option 3A
## 24: kVK_RightOption 3D
## 25: GDK_KEY_Alt_L FFE9
## 26: GDK_KEY_Alt_R \n FFEA
## 27: Qt::Key_Alt 01000023
## key_name space hex
The table above has one row for every occurence of the hex code in the original data, but there is no column for DOM code. To get that, we can use the code below:
(dom.hex.all <- dom.rest.dt[, nc::capture_all_str(
rest, key_name_hex_pattern
), by=dom_name])
## dom_name key_name space hex
## <char> <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Backspace kVK_Delete 33
## 3: Backspace GDK_KEY_BackSpace FF08
## 4: Backspace Qt::Key_Backspace \n 01000003
## 5: Enter VK_RETURN 0D
## 6: Enter kVK_Return 24
## 7: Enter kVK_ANSI_KeypadEnter \n 4C
## 8: Enter kVK_Powerbook_KeypadEnter 34
## 9: Enter GDK_KEY_Return FF0D
## 10: Enter GDK_KEY_KP_Enter \n FF8D
## 11: Enter GDK_KEY_ISO_Enter FE34
## 12: Enter GDK_KEY_3270_Enter \n FD1E
## 13: Enter Qt::Key_Return 01000004
## 14: Enter Qt::Key_Enter \n 01000005
## 15: ArrowDown VK_DOWN 28
## 16: ArrowDown kVK_DownArrow 7D
## 17: ArrowDown GDK_KEY_Down FF54
## 18: ArrowDown GDK_KEY_KP_Down \n FF99
## 19: ArrowDown Qt::Key_Down 01000015
## 20: Alt VK_MENU 12
## 21: Alt VK_LMENU A4
## 22: Alt VK_RMENU \n A5
## 23: Alt kVK_Option 3A
## 24: Alt kVK_RightOption 3D
## 25: Alt GDK_KEY_Alt_L FFE9
## 26: Alt GDK_KEY_Alt_R \n FFEA
## 27: Alt Qt::Key_Alt 01000023
## dom_name key_name space hex
The output above has the dom_name
column in addition to the others
shown previously. To just get the windows virtual key codes, we can do:
(dom.hex.windows <- dom.hex.all[grepl("^VK_", key_name)])
## dom_name key_name space hex
## <char> <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Enter VK_RETURN 0D
## 3: ArrowDown VK_DOWN 28
## 4: Alt VK_MENU 12
## 5: Alt VK_LMENU A4
## 6: Alt VK_RMENU \n A5
In the code below we keep just the first windows key code found:
(dom.hex.first <- dom.hex.windows[, .SD[1], by=dom_name])
## dom_name key_name space hex
## <char> <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Enter VK_RETURN 0D
## 3: ArrowDown VK_DOWN 28
## 4: Alt VK_MENU 12
We see that these DOM key names and windows virtual key codes are
consistent with those in the existing animint2
tests:
animint2.expected
## dom_name key_name hex
## <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Enter VK_RETURN 0D
## 3: ArrowDown VK_DOWN 28
Making a function
Next, make a function which runs the whole process from beginning to end.
get_dom_win_hex <- function(in.html){
nc::capture_all_str(
in.html, dom_name_rest_pattern
)[, {
nc::capture_all_str(
rest, key_name_hex_pattern
)[
grepl("^VK_", key_name), .SD[1]
]
}, by=dom_name]
}
get_dom_win_hex(some.html)
## dom_name key_name space hex
## <char> <char> <char> <char>
## 1: Backspace VK_BACK 08
## 2: Enter VK_RETURN 0D
## 3: ArrowDown VK_DOWN 28
## 4: Alt VK_MENU 12
## 5: Dead <NA> <NA> <NA>
The table above additionally contains rows with NA
for DOM names
that had no corresponding hex codes.
Using it on the whole data set
(full_win_codes <- get_dom_win_hex(html.lines))
## dom_name key_name space hex
## <char> <char> <char> <char>
## 1: Unidentified <NA> <NA> <NA>
## 2: Alt VK_MENU 12
## 3: AltGraph <NA> <NA> <NA>
## 4: CapsLock VK_CAPITAL 14
## 5: Control VK_CONTROL 11
## ---
## 314: Add VK_ADD 6B
## 315: Divide VK_DIVIDE 6F
## 316: Subtract VK_SUBTRACT 6D
## 317: Separator VK_SEPARATOR 6C
## 318: 0 VK_NUMPAD0 60
The table above could be used to define a new key2hex_code
variable,
which would expand the keys supported in animint2
tests:
key2hex_code <- full_win_codes[, structure(hex, names=dom_name)]
dput(key2hex_code)
## c(Unidentified = NA, Alt = "12", AltGraph = NA, CapsLock = "14",
## Control = "11", Fn = NA, FnLock = NA, Hyper = NA, Meta = "5B",
## NumLock = "90", ScrollLock = "91", Shift = "10", Super = NA,
## Symbol = NA, SymbolLock = NA, Enter = "0D", Tab = "09", ` ` = "20",
## ArrowDown = "28", ArrowLeft = "25", ArrowRight = "27", ArrowUp = "26",
## End = "23", Home = "24", PageDown = "22", PageUp = "21", Backspace = "08",
## Clear = "0C", Copy = NA, CrSel = "F7", Cut = NA, Delete = "2E",
## EraseEof = "F9", ExSel = "F8", Insert = "2D", Paste = NA, Redo = NA,
## Undo = NA, Accept = "1E", Again = NA, Attn = "F0", Cancel = NA,
## ContextMenu = "5D", Escape = "1B", Execute = "2B", Find = NA,
## Finish = "F1", Help = "2F", Pause = "13", Play = "FA", Props = NA,
## Select = "29", ZoomIn = NA, ZoomOut = NA, BrightnessDown = NA,
## BrightnessUp = NA, Eject = NA, LogOff = NA, Power = NA, PowerOff = NA,
## PrintScreen = "2C", Hibernate = NA, Standby = "5F", WakeUp = NA,
## AllCandidates = NA, Alphanumeric = "F0", CodeInput = NA, Compose = NA,
## Convert = "1C", Dead = NA, FinalMode = "18", GroupFirst = NA,
## GroupLast = NA, GroupNext = NA, GroupPrevious = NA, ModeChange = "1F",
## NextCandidate = NA, NonConvert = "1D", PreviousCandidate = NA,
## Process = "E5", SingleCandidate = NA, HangulMode = "15", HanjaMode = "19",
## JunjaMode = "17", Eisu = NA, Hankaku = "F3", Hiragana = "F2",
## HiraganaKatakana = NA, KanaMode = "15", KanjiMode = NA, Katakana = "F1",
## Romaji = "F5", Zenkaku = "F4", ZenkakuHanaku = NA, F1 = "70",
## F2 = "71", F3 = "72", F4 = "73", F5 = "74", F6 = "75", F7 = "76",
## F8 = "77", F9 = "78", F10 = "79", F11 = "7A", F12 = "7B", F13 = "7C",
## F14 = "7D", F15 = "7E", F16 = "7F", F17 = "80", F18 = "81", F19 = "82",
## F20 = "83", Soft1 = NA, Soft2 = NA, Soft3 = NA, Soft4 = NA, AppSwitch = NA,
## Call = NA, Camera = NA, CameraFocus = NA, EndCall = NA, GoBack = NA,
## GoHome = NA, HeadsetHook = NA, LastNumberRedial = NA, Notification = NA,
## MannerMode = NA, VoiceDial = NA, ChannelDown = NA, ChannelUp = NA,
## MediaFastForward = NA, MediaPause = NA, MediaPlay = NA, MediaPlayPause = "B3",
## MediaRecord = NA, MediaRewind = NA, MediaStop = "B2", MediaTrackNext = "B0",
## MediaTrackPrevious = "B1", AudioBalanceLeft = NA, AudioBalanceRight = NA,
## AudioBassDown = NA, AudioBassBoostDown = NA, AudioBassBoostToggle = NA,
## AudioBassBoostUp = NA, AudioBassUp = NA, AudioFaderFront = NA,
## AudioFaderRear = NA, AudioSurroundModeNext = NA, AudioTrebleDown = NA,
## AudioTrebleUp = NA, AudioVolumeDown = "AE", AudioVolumeMute = "AD",
## AudioVolumeUp = "AF", MicrophoneToggle = NA, MicrophoneVolumeDown = NA,
## MicrophoneVolumeMute = NA, MicrophoneVolumeUp = NA, TV = NA,
## TV3DMode = NA, TVAntennaCable = NA, TVAudioDescription = NA,
## TVAudioDescriptionMixDown = NA, TVAudioDescriptionMixUp = NA,
## TVContentsMenu = NA, TVDataService = NA, TVInput = NA, TVInputComponent1 = NA,
## TVInputComponent2 = NA, TVInputComposite1 = NA, TVInputComposite2 = NA,
## TVInputHDMI1 = NA, TVInputHDMI2 = NA, TVInputHDMI3 = NA, TVInputHDMI4 = NA,
## TVInputVGA1 = NA, TVMediaContext = NA, TVNetwork = NA, TVNumberEntry = NA,
## TVPower = NA, TVRadioService = NA, TVSatellite = NA, TVSatelliteBS = NA,
## TVSatelliteCS = NA, TVSatelliteToggle = NA, TVTerrestrialAnalog = NA,
## TVTerrestrialDigital = NA, TVTimer = NA, AVRInput = NA, AVRPower = NA,
## ColorF0Red = NA, ColorF1Green = NA, ColorF2Yellow = NA, ColorF3Blue = NA,
## ColorF4Grey = NA, ColorF5Brown = NA, ClosedCaptionToggle = NA,
## Dimmer = NA, DisplaySwap = NA, DVR = NA, Exit = NA, FavoriteClear0 = NA,
## FavoriteClear1 = NA, FavoriteClear2 = NA, FavoriteClear3 = NA,
## FavoriteRecall0 = NA, FavoriteRecall1 = NA, FavoriteRecall2 = NA,
## FavoriteRecall3 = NA, FavoriteStore0 = NA, FavoriteStore1 = NA,
## FavoriteStore2 = NA, FavoriteStore3 = NA, Guide = NA, GuideNextDay = NA,
## GuidePreviousDay = NA, Info = NA, InstantReplay = NA, Link = NA,
## ListProgram = NA, LiveContent = NA, Lock = NA, MediaApps = NA,
## MediaAudioTrack = NA, MediaLast = NA, MediaSkipBackward = NA,
## MediaSkipForward = NA, MediaStepBackward = NA, MediaStepForward = NA,
## MediaTopMenu = NA, NavigateIn = NA, NavigateNext = NA, NavigateOut = NA,
## NavigatePrevious = NA, NextFavoriteChannel = NA, NextUserProfile = NA,
## OnDemand = NA, Pairing = NA, PinPDown = NA, PinPMove = NA, PinPToggle = NA,
## PinPUp = NA, PlaySpeedDown = NA, PlaySpeedReset = NA, PlaySpeedUp = NA,
## RandomToggle = NA, RcLowBattery = NA, RecordSpeedNext = NA, RfBypass = NA,
## ScanChannelsToggle = NA, ScreenModeNext = NA, Settings = NA,
## SplitScreenToggle = NA, STBInput = NA, STBPower = NA, Subtitle = NA,
## Teletext = NA, VideoModeNext = NA, Wink = NA, ZoomToggle = "FB",
## SpeechCorrectionList = NA, SpeechInputToggle = NA, Close = NA,
## New = NA, Open = NA, Print = NA, Save = NA, SpellCheck = NA,
## MailForward = NA, MailReply = NA, MailSend = NA, LaunchCalculator = NA,
## LaunchCalendar = NA, LaunchContacts = NA, LaunchMail = "B4",
## LaunchMediaPlayer = "B5", LaunchMusicPlayer = NA, LaunchMyComputer = NA,
## LaunchPhone = NA, LaunchScreenSaver = NA, LaunchSpreadsheet = NA,
## LaunchWebBrowser = NA, LaunchWebCam = NA, LaunchWordProcessor = NA,
## LaunchApplication1 = "B6", LaunchApplication2 = "B7", LaunchApplication3 = NA,
## LaunchApplication4 = NA, LaunchApplication5 = NA, LaunchApplication6 = NA,
## LaunchApplication7 = NA, LaunchApplication8 = NA, LaunchApplication9 = NA,
## LaunchApplication10 = NA, LaunchApplication11 = NA, LaunchApplication12 = NA,
## LaunchApplication13 = NA, LaunchApplication14 = NA, LaunchApplication15 = NA,
## LaunchApplication16 = NA, BrowserBack = "A6", BrowserFavorites = "AB",
## BrowserForward = "A7", BrowserHome = "AC", BrowserRefresh = "A8",
## BrowserSearch = "AA", BrowserStop = "A9", Decimal = "6E", Key11 = NA,
## Key12 = NA, Multiply = "6A", Add = "6B", Divide = "6F", Subtract = "6D",
## Separator = "6C", `0` = "60")
Conclusions
When refactoring animint2
tests to use chromote, we saw how to use
regex to extract a table of hex codes from a web page with DOM key
names.
Session info
sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 22.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.10.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
##
## locale:
## [1] LC_CTYPE=fr_FR.UTF-8 LC_NUMERIC=C LC_TIME=fr_FR.UTF-8 LC_COLLATE=fr_FR.UTF-8
## [5] LC_MONETARY=fr_FR.UTF-8 LC_MESSAGES=fr_FR.UTF-8 LC_PAPER=fr_FR.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C
##
## time zone: America/New_York
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics utils datasets grDevices methods base
##
## other attached packages:
## [1] data.table_1.15.99
##
## loaded via a namespace (and not attached):
## [1] compiler_4.4.1 nc_2024.2.21 tools_4.4.1 knitr_1.47 xfun_0.45 evaluate_0.23