Skip to content

Commit dd4c1ab

Browse files
hvguptaaustinmycnathanfalletstephanlensky
authored
send_keys enhancement (#155)
* update * latest changes * update * will test later 👀 * saving changes * updated * update * save * update the keyorder and mroe testing * minor updates * final update * emoji and capitalisation handling * will test later * update * updates * updated the class structure * updating the elements * update * updating and removing unnecessary files * final changes * added change logs * removed the version number * some minor docstring changes * added a single test and changed the enter key * added escape key test * moving test files to correct folder + remove http server * ran the formatter and the linter * Update CHANGELOG.md Co-authored-by: Stephan Lensky <8302875+stephanlensky@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: Stephan Lensky <8302875+stephanlensky@users.noreply.github.com> * moved to `from_text` --------- Co-authored-by: Austin <austinmyc@gmail.com> Co-authored-by: NathanFallet <contact@nathanfallet.me> Co-authored-by: Stephan Lensky <8302875+stephanlensky@users.noreply.github.com>
1 parent dcbb164 commit dd4c1ab

File tree

9 files changed

+1395
-468
lines changed

9 files changed

+1395
-468
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- Complete rewrite of keyboard input system with new `KeyEvents` class in `zendriver.core.keys` @hvgupta
15+
- Added support for modifiers (Ctrl, Alt, Shift, Meta)
16+
- Added support for special keys including arrows, escape, delete and backspace
17+
- Added `KeyEvents.from_text()` class method for converting plain text to cdp events
18+
- Added `KeyEvents.from_mixed_input()` class method for handling mixed sequences of text, special keys to cdp events
19+
- Proper Handling of shift variants of keys
20+
- Comprehensive key event types: `CHAR`, `KEY_DOWN`, `KEY_UP`
21+
- Added key event type (`DOWN_AND_UP`) as a combination of `KEY_DOWN` and `KEY_UP`
22+
1423
### Changed
1524

25+
- `Element.send_keys()` now uses the new `KeyEvents` system (it is still backwards compatible with passing a string) @hvgupta
26+
- Key event processing now properly handles modifier key sequences @hvgupta
1627
- Update CDP schemas @nathanfallet
1728

1829
### Removed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ requires-python = ">=3.10"
2020
dependencies = [
2121
"asyncio-atexit>=1.0.1",
2222
"deprecated>=1.2.14",
23+
"emoji>=2.14.1",
2324
"grapheme>=0.6.0",
2425
"mss>=9.0.2",
2526
"websockets>=14.0",

tests/core/test_keyinputs.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import zendriver as zd
2+
3+
from tests.sample_data import sample_file
4+
from zendriver import SpecialKeys, KeyModifiers, KeyEvents
5+
6+
7+
async def test_visible_events(browser: zd.Browser):
8+
"""Test keyboard events with contenteditable div."""
9+
# Open the page
10+
main_page = await browser.get(sample_file("simple_editor.html"))
11+
12+
text_part = await main_page.find('//*[@id="editor"]')
13+
14+
await text_part.mouse_click("left")
15+
await main_page.sleep(1) # give some time to focus the text part
16+
await text_part.send_keys("Hello, world!")
17+
18+
payloads = KeyEvents.from_mixed_input(
19+
[
20+
" This is another sentence",
21+
SpecialKeys.ENTER,
22+
("a", KeyModifiers.Ctrl),
23+
("c", KeyModifiers.Ctrl),
24+
SpecialKeys.ARROW_UP,
25+
("v", KeyModifiers.Ctrl),
26+
" This is pasted text. 👍",
27+
]
28+
)
29+
30+
await text_part.send_keys(payloads)
31+
check_part = await main_page.find('//*[@id="editor"]')
32+
assert len(check_part.children) == 4, "Expected 4 children after operations"
33+
34+
expected_output = [
35+
"Hello, world! This is another sentence",
36+
"<div>&nbsp;This is pasted text. 👍</div>",
37+
"Hello, world! This is another sentence",
38+
"<div><br></div>",
39+
]
40+
41+
for expected, actual_text in zip(expected_output, check_part.children):
42+
actual_html = await actual_text.get_html()
43+
assert actual_html == expected, f"Expected '{expected}', got '{actual_html}'"
44+
45+
46+
async def test_escape_key_popup(browser: zd.Browser):
47+
"""Test escape key functionality to close a popup."""
48+
main_page = await browser.get(sample_file("special_key_detector.html"))
49+
50+
status_check = await main_page.find('//*[@id="status"]')
51+
assert (
52+
status_check.text == "Ready - Click button to open popUp"
53+
), "There is something wrong with the page"
54+
55+
button = await main_page.find('//*[@id="mainpageButton"]')
56+
await button.mouse_click("left")
57+
58+
await status_check
59+
assert (
60+
status_check.text == "popUp is OPEN - Press Escape to close"
61+
), "Popup did not open correctly"
62+
63+
pop_up = await main_page.find('//*[@id="mainpage"]/div')
64+
await pop_up.send_keys(SpecialKeys.ESCAPE)
65+
66+
await status_check
67+
assert (
68+
status_check.text == "popUp is CLOSED - Click button to open again"
69+
), "Popup did not close correctly"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Blank Page for Writing</title>
6+
<style>
7+
body {
8+
margin: 0;
9+
padding: 2rem;
10+
font-family: Arial, sans-serif;
11+
font-size: 1.2rem;
12+
background-color: #fff;
13+
color: #000;
14+
}
15+
#editor {
16+
width: 100%;
17+
height: 100vh;
18+
outline: none;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<div id="editor" contenteditable="true">
24+
</div>
25+
</body>
26+
</html>
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Escape Key mainpage Test</title>
7+
<style>
8+
body {
9+
margin: 0;
10+
padding: 2rem;
11+
font-family: Arial, sans-serif;
12+
font-size: 1.2rem;
13+
background-color: #fff;
14+
color: #000;
15+
}
16+
17+
.container {
18+
max-width: 600px;
19+
margin: 0 auto;
20+
text-align: center;
21+
}
22+
23+
#mainpageButton {
24+
background: #007bff;
25+
color: white;
26+
border: none;
27+
padding: 15px 30px;
28+
font-size: 18px;
29+
border-radius: 5px;
30+
cursor: pointer;
31+
margin: 20px;
32+
}
33+
34+
#mainpageButton:hover {
35+
background: #0056b3;
36+
}
37+
38+
/* Modal/mainpage styles */
39+
.modal {
40+
display: none;
41+
position: fixed;
42+
z-index: 1000;
43+
left: 0;
44+
top: 0;
45+
width: 100%;
46+
height: 100%;
47+
background-color: rgba(0, 0, 0, 0.5);
48+
}
49+
50+
.modal-content {
51+
background-color: white;
52+
margin: 15% auto;
53+
padding: 30px;
54+
border-radius: 10px;
55+
width: 400px;
56+
max-width: 90%;
57+
text-align: center;
58+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
59+
position: relative;
60+
}
61+
62+
.closeButton {
63+
position: absolute;
64+
top: 10px;
65+
right: 15px;
66+
font-size: 24px;
67+
font-weight: bold;
68+
color: #aaa;
69+
cursor: pointer;
70+
}
71+
72+
.closeButton:hover {
73+
color: #000;
74+
}
75+
76+
.mainpage-title {
77+
color: #333;
78+
margin-bottom: 20px;
79+
}
80+
81+
.mainpage-text {
82+
color: #666;
83+
margin-bottom: 20px;
84+
line-height: 1.5;
85+
}
86+
87+
.popUp-footer {
88+
margin-top: 20px;
89+
padding-top: 20px;
90+
border-top: 1px solid #eee;
91+
color: #888;
92+
font-size: 14px;
93+
}
94+
95+
/* Status indicator */
96+
.status {
97+
margin: 20px 0;
98+
padding: 10px;
99+
border-radius: 5px;
100+
background: #ffffff;
101+
}
102+
103+
.status.popUp-open {
104+
background: #ffffff;
105+
}
106+
107+
.status.popUp-closed {
108+
background: #ffffff;
109+
}
110+
</style>
111+
</head>
112+
<body>
113+
<div class="container">
114+
<h1>Escape Key popUp Test</h1>
115+
<p>Click the button below to open a popUp. Press <strong>Escape</strong> to close it.</p>
116+
117+
<button id="mainpageButton">Open popUp</button>
118+
119+
<div id="status" class="status">Ready - Click button to open popUp</div>
120+
</div>
121+
122+
123+
<div id="mainpage" class="modal">
124+
<div class="modal-content">
125+
<h2 class="mainpage-title">Test popUp</h2>
126+
<p class="mainpage-text">
127+
This is a test popUp that can be closed with the Escape key.
128+
</p>
129+
<div class="popUp-footer">
130+
Press <strong>Escape</strong> to close this popUp
131+
</div>
132+
</div>
133+
</div>
134+
135+
<script>
136+
const mainpage = document.getElementById('mainpage');
137+
const openButton = document.getElementById('mainpageButton');
138+
const status = document.getElementById('status');
139+
140+
// Track popUp state
141+
let isPopUpOpen = false;
142+
143+
function openPopUp() {
144+
mainpage.style.display = 'block';
145+
isPopUpOpen = true;
146+
status.textContent = 'popUp is OPEN - Press Escape to close';
147+
status.className = 'status popUp-open';
148+
149+
// Focus the popUp for better keyboard interaction
150+
mainpage.focus();
151+
152+
window.popUpState = 'open';
153+
}
154+
155+
function closePopUp() {
156+
mainpage.style.display = 'none';
157+
isPopUpOpen = false;
158+
status.textContent = 'popUp is CLOSED - Click button to open again';
159+
status.className = 'status popUp-closed';
160+
161+
// Return focus to the open button
162+
openButton.focus();
163+
164+
window.isPopUpOpen = 'closed';
165+
}
166+
167+
// Open popUp when button is clicked
168+
openButton.addEventListener('click', openPopUp);
169+
170+
// Close popUp when Escape key is pressed
171+
document.addEventListener('keydown', function(event) {
172+
if (event.key === 'Escape' || event.keyCode === 27) {
173+
if (isPopUpOpen) {
174+
closePopUp();
175+
event.preventDefault(); // Prevent other escape behaviors
176+
}
177+
}
178+
});
179+
180+
// Functions for zendriver testing
181+
window.openTestmainpage = openPopUp;
182+
window.closeTestmainpage = closePopUp;
183+
window.ismainpageOpen = () => ismainpageOpen;
184+
window.getmainpageState = () => window.mainpageState || 'closed';
185+
186+
// Wait for mainpage to open/close
187+
window.waitFormainpageState = (expectedState, timeout = 5000) => {
188+
return new Promise((resolve, reject) => {
189+
const startTime = Date.now();
190+
const checkState = () => {
191+
const currentState = ismainpageOpen ? 'open' : 'closed';
192+
if (currentState === expectedState) {
193+
resolve(currentState);
194+
} else if (Date.now() - startTime > timeout) {
195+
reject(new Error(`Timeout waiting for mainpage state: ${expectedState}`));
196+
} else {
197+
setTimeout(checkState, 100);
198+
}
199+
};
200+
checkState();
201+
});
202+
};
203+
204+
window.mainpageState = 'closed';
205+
</script>
206+
</body>
207+
</html>

0 commit comments

Comments
 (0)