craftdriver 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/cli/snapshot.js +31 -4
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/lib/browser.d.ts +6 -0
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +6 -0
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/by.d.ts +18 -0
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +141 -17
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/locator.d.ts +28 -0
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +28 -0
- package/dist/lib/locator.js.map +1 -1
- package/docs/browser-api.md +4 -1
- package/docs/public/examples/a11y.html +44 -0
- package/docs/public/examples/clock.html +83 -0
- package/docs/public/examples/console-errors.html +344 -0
- package/docs/public/examples/dialogs.html +42 -0
- package/docs/public/examples/download.html +31 -0
- package/docs/public/examples/dynamic.html +131 -0
- package/docs/public/examples/emulate.html +127 -0
- package/docs/public/examples/evaluate.html +28 -0
- package/docs/public/examples/hover-select.html +295 -0
- package/docs/public/examples/iframe-child.html +51 -0
- package/docs/public/examples/iframes.html +35 -0
- package/docs/public/examples/keyboard.html +195 -0
- package/docs/public/examples/locator.html +131 -0
- package/docs/public/examples/login.html +116 -0
- package/docs/public/examples/mobile.html +270 -0
- package/docs/public/examples/mouse.html +505 -0
- package/docs/public/examples/network.html +293 -0
- package/docs/public/examples/popup-target.html +26 -0
- package/docs/public/examples/popup.html +50 -0
- package/docs/public/examples/screenshot.html +99 -0
- package/docs/public/examples/selectors.html +150 -0
- package/docs/public/examples/session.html +514 -0
- package/docs/public/examples/upload.html +36 -0
- package/docs/recipes/accessibility-gate.md +24 -40
- package/docs/recipes/console-error-gate.md +29 -38
- package/docs/recipes/file-upload-download.md +26 -43
- package/docs/recipes/find-elements.md +142 -0
- package/docs/recipes/login-once-reuse-session.md +24 -44
- package/docs/recipes/mobile-flow-with-network-and-logs.md +26 -41
- package/docs/recipes/mock-api-and-assert-network.md +22 -35
- package/docs/recipes/multi-user-contexts.md +32 -43
- package/docs/recipes/page-objects.md +52 -0
- package/docs/recipes/trace-failing-test.md +31 -44
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
- package/docs/recipes/vitest-browser-lifecycle.md +14 -11
- package/docs/recipes.md +20 -1
- package/docs/selectors.md +291 -229
- package/package.json +5 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>Evaluate Playground</title>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Evaluate Playground</h1>
|
|
11
|
+
|
|
12
|
+
<!-- target element for element.evaluate() tests -->
|
|
13
|
+
<button id="action-btn" class="primary active">Click me</button>
|
|
14
|
+
|
|
15
|
+
<!-- state exposed on window.__state for evaluate() tests -->
|
|
16
|
+
<script>
|
|
17
|
+
window.__state = {
|
|
18
|
+
count: 0,
|
|
19
|
+
name: 'craftdriver',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
document.getElementById('action-btn').addEventListener('click', function () {
|
|
23
|
+
window.__state.count++;
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
</body>
|
|
27
|
+
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Hover & Select Examples</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
max-width: 800px;
|
|
12
|
+
margin: 40px auto;
|
|
13
|
+
padding: 20px;
|
|
14
|
+
background: #f5f5f5;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.card {
|
|
18
|
+
background: white;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
margin-bottom: 20px;
|
|
22
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
h1 {
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h2 {
|
|
30
|
+
color: #666;
|
|
31
|
+
margin-top: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Tooltip */
|
|
35
|
+
.tooltip-container {
|
|
36
|
+
position: relative;
|
|
37
|
+
display: inline-block;
|
|
38
|
+
padding: 10px 20px;
|
|
39
|
+
background: #007bff;
|
|
40
|
+
color: white;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.tooltip {
|
|
46
|
+
visibility: hidden;
|
|
47
|
+
opacity: 0;
|
|
48
|
+
position: absolute;
|
|
49
|
+
bottom: 125%;
|
|
50
|
+
left: 50%;
|
|
51
|
+
transform: translateX(-50%);
|
|
52
|
+
background: #333;
|
|
53
|
+
color: white;
|
|
54
|
+
padding: 8px 12px;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
transition: opacity 0.2s;
|
|
59
|
+
z-index: 1000;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.tooltip::after {
|
|
63
|
+
content: '';
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: 100%;
|
|
66
|
+
left: 50%;
|
|
67
|
+
margin-left: -5px;
|
|
68
|
+
border-width: 5px;
|
|
69
|
+
border-style: solid;
|
|
70
|
+
border-color: #333 transparent transparent transparent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tooltip-container:hover .tooltip {
|
|
74
|
+
visibility: visible;
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Dropdown menu */
|
|
79
|
+
.dropdown {
|
|
80
|
+
position: relative;
|
|
81
|
+
display: inline-block;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.dropdown-button {
|
|
85
|
+
background: #28a745;
|
|
86
|
+
color: white;
|
|
87
|
+
padding: 10px 20px;
|
|
88
|
+
border: none;
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.dropdown-menu {
|
|
94
|
+
visibility: hidden;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: 100%;
|
|
98
|
+
left: 0;
|
|
99
|
+
background: white;
|
|
100
|
+
min-width: 200px;
|
|
101
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
102
|
+
border-radius: 4px;
|
|
103
|
+
margin-top: 5px;
|
|
104
|
+
transition: opacity 0.2s;
|
|
105
|
+
z-index: 1000;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dropdown:hover .dropdown-menu {
|
|
109
|
+
visibility: visible;
|
|
110
|
+
opacity: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dropdown-item {
|
|
114
|
+
padding: 10px 15px;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
border-bottom: 1px solid #eee;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.dropdown-item:last-child {
|
|
120
|
+
border-bottom: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dropdown-item:hover {
|
|
124
|
+
background: #f5f5f5;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Selects */
|
|
128
|
+
select {
|
|
129
|
+
padding: 10px;
|
|
130
|
+
border: 1px solid #ced4da;
|
|
131
|
+
border-radius: 4px;
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
min-width: 200px;
|
|
134
|
+
margin-bottom: 10px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.result {
|
|
138
|
+
background: #f8f9fa;
|
|
139
|
+
border: 1px solid #dee2e6;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
padding: 15px;
|
|
142
|
+
margin-top: 15px;
|
|
143
|
+
font-family: monospace;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
label {
|
|
147
|
+
display: block;
|
|
148
|
+
margin-top: 15px;
|
|
149
|
+
margin-bottom: 5px;
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
153
|
+
</head>
|
|
154
|
+
|
|
155
|
+
<body>
|
|
156
|
+
<h1>🎯 Hover & Select Examples</h1>
|
|
157
|
+
<p>Test page for hover interactions and native select elements.</p>
|
|
158
|
+
|
|
159
|
+
<!-- Tooltip Example -->
|
|
160
|
+
<div class="card">
|
|
161
|
+
<h2>1. Tooltip on Hover</h2>
|
|
162
|
+
<p>Hover over the button to see a tooltip:</p>
|
|
163
|
+
|
|
164
|
+
<div class="tooltip-container" id="tooltip-trigger">
|
|
165
|
+
Hover Me
|
|
166
|
+
<span class="tooltip" id="tooltip-text">This is a helpful tooltip!</span>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div id="tooltip-status" class="result">Tooltip not shown</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Dropdown Menu Example -->
|
|
173
|
+
<div class="card">
|
|
174
|
+
<h2>2. Dropdown Menu on Hover</h2>
|
|
175
|
+
<p>Hover over the button to reveal menu:</p>
|
|
176
|
+
|
|
177
|
+
<div class="dropdown">
|
|
178
|
+
<button class="dropdown-button" id="dropdown-trigger">Menu ▼</button>
|
|
179
|
+
<div class="dropdown-menu" id="dropdown-menu">
|
|
180
|
+
<div class="dropdown-item" id="menu-item-1" onclick="selectMenuItem('Profile')">👤 Profile</div>
|
|
181
|
+
<div class="dropdown-item" id="menu-item-2" onclick="selectMenuItem('Settings')">⚙️ Settings</div>
|
|
182
|
+
<div class="dropdown-item" id="menu-item-3" onclick="selectMenuItem('Logout')">🚪 Logout</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div id="menu-selection" class="result">No menu item selected</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Native Select Elements -->
|
|
190
|
+
<div class="card">
|
|
191
|
+
<h2>3. Native Select Dropdowns</h2>
|
|
192
|
+
|
|
193
|
+
<label for="language-select">Language:</label>
|
|
194
|
+
<select id="language-select" onchange="updateSelection()">
|
|
195
|
+
<option value="">-- Choose Language --</option>
|
|
196
|
+
<option value="en">English</option>
|
|
197
|
+
<option value="es">Spanish</option>
|
|
198
|
+
<option value="fr">French</option>
|
|
199
|
+
<option value="de">German</option>
|
|
200
|
+
<option value="ja">Japanese</option>
|
|
201
|
+
</select>
|
|
202
|
+
|
|
203
|
+
<label for="country-select">Country:</label>
|
|
204
|
+
<select id="country-select" onchange="updateSelection()">
|
|
205
|
+
<option value="">-- Choose Country --</option>
|
|
206
|
+
<option value="us">United States</option>
|
|
207
|
+
<option value="uk">United Kingdom</option>
|
|
208
|
+
<option value="ca">Canada</option>
|
|
209
|
+
<option value="au">Australia</option>
|
|
210
|
+
<option value="jp">Japan</option>
|
|
211
|
+
</select>
|
|
212
|
+
|
|
213
|
+
<label for="framework-select">Framework:</label>
|
|
214
|
+
<select id="framework-select" onchange="updateSelection()">
|
|
215
|
+
<option value="">-- Choose Framework --</option>
|
|
216
|
+
<option value="react">React</option>
|
|
217
|
+
<option value="vue">Vue</option>
|
|
218
|
+
<option value="angular">Angular</option>
|
|
219
|
+
<option value="svelte">Svelte</option>
|
|
220
|
+
</select>
|
|
221
|
+
|
|
222
|
+
<div id="select-result" class="result">Make selections above...</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Hover State Change -->
|
|
226
|
+
<div class="card">
|
|
227
|
+
<h2>4. Hover State Detection</h2>
|
|
228
|
+
<p>These elements change state on hover:</p>
|
|
229
|
+
|
|
230
|
+
<div id="hover-box"
|
|
231
|
+
style="display: inline-block; padding: 20px; background: #007bff; color: white; border-radius: 4px; cursor: pointer; transition: all 0.2s;">
|
|
232
|
+
Hover Box
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div id="hover-box-status" class="result" style="margin-top: 10px;">Hover state: not hovered</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<script>
|
|
239
|
+
// Tooltip tracking
|
|
240
|
+
const tooltipTrigger = document.getElementById('tooltip-trigger');
|
|
241
|
+
const tooltipStatus = document.getElementById('tooltip-status');
|
|
242
|
+
|
|
243
|
+
tooltipTrigger.addEventListener('mouseenter', () => {
|
|
244
|
+
tooltipStatus.textContent = 'Tooltip is visible';
|
|
245
|
+
tooltipStatus.style.background = '#d4edda';
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
tooltipTrigger.addEventListener('mouseleave', () => {
|
|
249
|
+
tooltipStatus.textContent = 'Tooltip hidden';
|
|
250
|
+
tooltipStatus.style.background = '#f8f9fa';
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Dropdown menu item selection
|
|
254
|
+
function selectMenuItem(item) {
|
|
255
|
+
document.getElementById('menu-selection').textContent = `Selected: ${item}`;
|
|
256
|
+
document.getElementById('menu-selection').style.background = '#d4edda';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Select updates
|
|
260
|
+
function updateSelection() {
|
|
261
|
+
const language = document.getElementById('language-select').value;
|
|
262
|
+
const country = document.getElementById('country-select').value;
|
|
263
|
+
const framework = document.getElementById('framework-select').value;
|
|
264
|
+
|
|
265
|
+
const parts = [];
|
|
266
|
+
if (language) parts.push(`Language: ${language}`);
|
|
267
|
+
if (country) parts.push(`Country: ${country}`);
|
|
268
|
+
if (framework) parts.push(`Framework: ${framework}`);
|
|
269
|
+
|
|
270
|
+
const result = document.getElementById('select-result');
|
|
271
|
+
result.textContent = parts.length > 0 ? parts.join(' | ') : 'Make selections above...';
|
|
272
|
+
result.style.background = parts.length > 0 ? '#d4edda' : '#f8f9fa';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Hover box
|
|
276
|
+
const hoverBox = document.getElementById('hover-box');
|
|
277
|
+
const hoverBoxStatus = document.getElementById('hover-box-status');
|
|
278
|
+
|
|
279
|
+
hoverBox.addEventListener('mouseenter', () => {
|
|
280
|
+
hoverBox.style.background = '#0056b3';
|
|
281
|
+
hoverBox.style.transform = 'scale(1.05)';
|
|
282
|
+
hoverBoxStatus.textContent = 'Hover state: hovered';
|
|
283
|
+
hoverBoxStatus.style.background = '#d4edda';
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
hoverBox.addEventListener('mouseleave', () => {
|
|
287
|
+
hoverBox.style.background = '#007bff';
|
|
288
|
+
hoverBox.style.transform = 'scale(1)';
|
|
289
|
+
hoverBoxStatus.textContent = 'Hover state: not hovered';
|
|
290
|
+
hoverBoxStatus.style.background = '#f8f9fa';
|
|
291
|
+
});
|
|
292
|
+
</script>
|
|
293
|
+
</body>
|
|
294
|
+
|
|
295
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Iframe Child Page</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
margin: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
button {
|
|
15
|
+
padding: 8px 14px;
|
|
16
|
+
border: none;
|
|
17
|
+
border-radius: 6px;
|
|
18
|
+
background: #2563eb;
|
|
19
|
+
color: #fff;
|
|
20
|
+
font-weight: 600;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
button:hover {
|
|
25
|
+
background: #1d4ed8;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#child-result {
|
|
29
|
+
margin-top: 0.5rem;
|
|
30
|
+
font-weight: bold;
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
|
|
35
|
+
<body>
|
|
36
|
+
<h2>Child Frame</h2>
|
|
37
|
+
<button id="child-btn">Click Me</button>
|
|
38
|
+
<p id="child-result"></p>
|
|
39
|
+
<input id="child-input" type="text" placeholder="type here" />
|
|
40
|
+
<p id="child-value"></p>
|
|
41
|
+
<script>
|
|
42
|
+
document.getElementById('child-btn').addEventListener('click', function () {
|
|
43
|
+
document.getElementById('child-result').textContent = 'clicked';
|
|
44
|
+
});
|
|
45
|
+
document.getElementById('child-input').addEventListener('input', function () {
|
|
46
|
+
document.getElementById('child-value').textContent = this.value;
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
|
|
51
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Craftdriver Iframes</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
margin: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
iframe {
|
|
15
|
+
border: 2px solid #ddd;
|
|
16
|
+
border-radius: 8px;
|
|
17
|
+
width: 480px;
|
|
18
|
+
height: 200px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#parent-status {
|
|
22
|
+
margin-top: 0.5rem;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
|
|
28
|
+
<body>
|
|
29
|
+
<h1>Iframe Test Page</h1>
|
|
30
|
+
<p>Parent page content.</p>
|
|
31
|
+
<p id="parent-status"></p>
|
|
32
|
+
<iframe id="my-frame" src="/iframe-child.html"></iframe>
|
|
33
|
+
</body>
|
|
34
|
+
|
|
35
|
+
</html>
|
|
@@ -0,0 +1,195 @@
|
|
|
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>Keyboard Actions Playground</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f1115;
|
|
10
|
+
--panel: #151925;
|
|
11
|
+
--accent: #4f86ff;
|
|
12
|
+
--text: #e6edf3;
|
|
13
|
+
--muted: #9aa5b1;
|
|
14
|
+
--ok: #18a558;
|
|
15
|
+
}
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
html,
|
|
20
|
+
body {
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
body {
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 16px;
|
|
26
|
+
color: var(--text);
|
|
27
|
+
background: var(--bg);
|
|
28
|
+
font-family:
|
|
29
|
+
ui-sans-serif,
|
|
30
|
+
system-ui,
|
|
31
|
+
-apple-system,
|
|
32
|
+
Segoe UI,
|
|
33
|
+
Roboto,
|
|
34
|
+
Ubuntu,
|
|
35
|
+
Cantarell,
|
|
36
|
+
Noto Sans,
|
|
37
|
+
Helvetica Neue,
|
|
38
|
+
Arial;
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: 2fr 1fr;
|
|
41
|
+
gap: 12px;
|
|
42
|
+
}
|
|
43
|
+
h1 {
|
|
44
|
+
font-size: 18px;
|
|
45
|
+
margin: 0 0 8px;
|
|
46
|
+
}
|
|
47
|
+
.card {
|
|
48
|
+
background: var(--panel);
|
|
49
|
+
border: 1px solid #222839;
|
|
50
|
+
border-radius: 10px;
|
|
51
|
+
padding: 12px;
|
|
52
|
+
}
|
|
53
|
+
.row {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
56
|
+
gap: 12px;
|
|
57
|
+
}
|
|
58
|
+
textarea,
|
|
59
|
+
input {
|
|
60
|
+
width: 100%;
|
|
61
|
+
background: #0c1426;
|
|
62
|
+
color: var(--text);
|
|
63
|
+
border: 1px solid #28314a;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
padding: 10px;
|
|
66
|
+
}
|
|
67
|
+
.mono {
|
|
68
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
69
|
+
}
|
|
70
|
+
.pill {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
gap: 6px;
|
|
73
|
+
background: #101628;
|
|
74
|
+
border: 1px solid #1f2740;
|
|
75
|
+
color: var(--muted);
|
|
76
|
+
padding: 4px 8px;
|
|
77
|
+
border-radius: 999px;
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
}
|
|
80
|
+
.value {
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
.ok {
|
|
84
|
+
color: var(--ok);
|
|
85
|
+
}
|
|
86
|
+
.logline {
|
|
87
|
+
color: #d1d5db;
|
|
88
|
+
font-size: 12px;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<main class="card">
|
|
94
|
+
<h1>Keyboard Actions Playground</h1>
|
|
95
|
+
|
|
96
|
+
<section class="card" style="margin-top: 8px">
|
|
97
|
+
<h2 style="font-size: 14px; margin: 0 0 8px">Editor (type / backspace / enter)</h2>
|
|
98
|
+
<textarea id="editor" rows="6" placeholder="Click here, then type..."></textarea>
|
|
99
|
+
<div style="margin-top: 8px; display: flex; gap: 12px; align-items: center">
|
|
100
|
+
<span class="pill">Lines: <span id="lineCount" class="value">0</span></span>
|
|
101
|
+
<span class="pill">Last key: <span id="lastKey" class="value">n/a</span></span>
|
|
102
|
+
<span class="pill">Modifiers: <span id="modifiers" class="value">none</span></span>
|
|
103
|
+
</div>
|
|
104
|
+
<div style="margin-top: 8px">
|
|
105
|
+
<div class="pill">Mirror</div>
|
|
106
|
+
<pre id="mirror" class="mono" style="margin: 6px 0 0; white-space: pre-wrap"></pre>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<div class="row" style="margin-top: 12px">
|
|
111
|
+
<section class="card">
|
|
112
|
+
<h2 style="font-size: 14px; margin: 0 0 8px">Enter key action</h2>
|
|
113
|
+
<input id="enterTarget" placeholder="Focus here and press Enter" />
|
|
114
|
+
<div style="margin-top: 8px" class="pill">
|
|
115
|
+
Result: <span id="enterResult" class="value">idle</span>
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<section class="card">
|
|
120
|
+
<h2 style="font-size: 14px; margin: 0 0 8px">Help</h2>
|
|
121
|
+
<div class="logline">• Click the editor before using keyboard.type/press.</div>
|
|
122
|
+
<div class="logline">• Backspace removes characters; Enter creates a new line.</div>
|
|
123
|
+
<div class="logline">• Use Control+A then Backspace to clear.</div>
|
|
124
|
+
<div class="logline">• Enter inside the input sets the result to "submitted".</div>
|
|
125
|
+
</section>
|
|
126
|
+
</div>
|
|
127
|
+
</main>
|
|
128
|
+
|
|
129
|
+
<aside class="card">
|
|
130
|
+
<h2 style="font-size: 14px; margin: 0 0 8px">Status</h2>
|
|
131
|
+
<div>Active element: <span id="activeEl" class="value mono">n/a</span></div>
|
|
132
|
+
</aside>
|
|
133
|
+
|
|
134
|
+
<script>
|
|
135
|
+
(function () {
|
|
136
|
+
const $ = (s) => document.querySelector(s);
|
|
137
|
+
const editor = $('#editor');
|
|
138
|
+
const mirror = $('#mirror');
|
|
139
|
+
const lineCount = $('#lineCount');
|
|
140
|
+
const lastKey = $('#lastKey');
|
|
141
|
+
const modsEl = $('#modifiers');
|
|
142
|
+
const enterTarget = $('#enterTarget');
|
|
143
|
+
const enterResult = $('#enterResult');
|
|
144
|
+
const activeEl = $('#activeEl');
|
|
145
|
+
|
|
146
|
+
function updateActive() {
|
|
147
|
+
const el = document.activeElement;
|
|
148
|
+
if (!el) {
|
|
149
|
+
activeEl.textContent = 'none';
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const id = el.id ? '#' + el.id : el.tagName.toLowerCase();
|
|
153
|
+
activeEl.textContent = id;
|
|
154
|
+
}
|
|
155
|
+
updateActive();
|
|
156
|
+
document.addEventListener('focusin', updateActive);
|
|
157
|
+
document.addEventListener('focusout', updateActive);
|
|
158
|
+
|
|
159
|
+
function updateMirror() {
|
|
160
|
+
const v = editor.value;
|
|
161
|
+
mirror.textContent = v;
|
|
162
|
+
const lines = v.length ? v.split('\n').length : 0;
|
|
163
|
+
lineCount.textContent = String(lines);
|
|
164
|
+
}
|
|
165
|
+
editor.addEventListener('input', updateMirror);
|
|
166
|
+
|
|
167
|
+
// Track last key and modifiers globally (document level)
|
|
168
|
+
const mods = new Set();
|
|
169
|
+
function renderMods() {
|
|
170
|
+
modsEl.textContent = mods.size ? Array.from(mods).join('+') : 'none';
|
|
171
|
+
}
|
|
172
|
+
document.addEventListener('keydown', (e) => {
|
|
173
|
+
lastKey.textContent = e.key;
|
|
174
|
+
if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Alt' || e.key === 'Meta') {
|
|
175
|
+
mods.add(e.key);
|
|
176
|
+
renderMods();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
document.addEventListener('keyup', (e) => {
|
|
180
|
+
if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Alt' || e.key === 'Meta') {
|
|
181
|
+
mods.delete(e.key);
|
|
182
|
+
renderMods();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Enter action in input
|
|
187
|
+
enterTarget.addEventListener('keydown', (e) => {
|
|
188
|
+
if (e.key === 'Enter') {
|
|
189
|
+
enterResult.textContent = 'submitted';
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
})();
|
|
193
|
+
</script>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|