craftdriver 1.1.0 → 1.2.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/cli/snapshot.js +31 -4
  4. package/dist/cli/snapshot.js.map +1 -1
  5. package/dist/lib/bidi/index.d.ts +4 -5
  6. package/dist/lib/bidi/index.d.ts.map +1 -1
  7. package/dist/lib/bidi/index.js +18 -8
  8. package/dist/lib/bidi/index.js.map +1 -1
  9. package/dist/lib/bidi/logs.d.ts +12 -1
  10. package/dist/lib/bidi/logs.d.ts.map +1 -1
  11. package/dist/lib/bidi/logs.js +20 -8
  12. package/dist/lib/bidi/logs.js.map +1 -1
  13. package/dist/lib/browser.d.ts +6 -9
  14. package/dist/lib/browser.d.ts.map +1 -1
  15. package/dist/lib/browser.js +6 -4
  16. package/dist/lib/browser.js.map +1 -1
  17. package/dist/lib/by.d.ts +18 -0
  18. package/dist/lib/by.d.ts.map +1 -1
  19. package/dist/lib/by.js +141 -17
  20. package/dist/lib/by.js.map +1 -1
  21. package/dist/lib/locator.d.ts +28 -0
  22. package/dist/lib/locator.d.ts.map +1 -1
  23. package/dist/lib/locator.js +28 -0
  24. package/dist/lib/locator.js.map +1 -1
  25. package/docs/browser-api.md +4 -2
  26. package/docs/browser-logs.md +8 -23
  27. package/docs/public/examples/a11y.html +44 -0
  28. package/docs/public/examples/clock.html +83 -0
  29. package/docs/public/examples/console-errors.html +344 -0
  30. package/docs/public/examples/dialogs.html +42 -0
  31. package/docs/public/examples/download.html +31 -0
  32. package/docs/public/examples/dynamic.html +131 -0
  33. package/docs/public/examples/emulate.html +127 -0
  34. package/docs/public/examples/evaluate.html +28 -0
  35. package/docs/public/examples/hover-select.html +295 -0
  36. package/docs/public/examples/iframe-child.html +51 -0
  37. package/docs/public/examples/iframes.html +35 -0
  38. package/docs/public/examples/keyboard.html +195 -0
  39. package/docs/public/examples/locator.html +131 -0
  40. package/docs/public/examples/login.html +116 -0
  41. package/docs/public/examples/mobile.html +270 -0
  42. package/docs/public/examples/mouse.html +505 -0
  43. package/docs/public/examples/network.html +293 -0
  44. package/docs/public/examples/popup-target.html +26 -0
  45. package/docs/public/examples/popup.html +50 -0
  46. package/docs/public/examples/screenshot.html +99 -0
  47. package/docs/public/examples/selectors.html +150 -0
  48. package/docs/public/examples/session.html +514 -0
  49. package/docs/public/examples/upload.html +36 -0
  50. package/docs/recipes/accessibility-gate.md +24 -40
  51. package/docs/recipes/console-error-gate.md +27 -39
  52. package/docs/recipes/file-upload-download.md +24 -44
  53. package/docs/recipes/find-elements.md +142 -0
  54. package/docs/recipes/login-once-reuse-session.md +22 -45
  55. package/docs/recipes/mobile-flow-with-network-and-logs.md +22 -41
  56. package/docs/recipes/mock-api-and-assert-network.md +22 -35
  57. package/docs/recipes/multi-user-contexts.md +32 -43
  58. package/docs/recipes/page-objects.md +52 -0
  59. package/docs/recipes/trace-failing-test.md +31 -44
  60. package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
  61. package/docs/recipes/vitest-browser-lifecycle.md +15 -15
  62. package/docs/recipes.md +20 -1
  63. package/docs/selectors.md +291 -229
  64. package/docs/tracing.md +2 -2
  65. package/package.json +5 -1
  66. package/skills/craftdriver/SKILL.md +3 -2
  67. package/skills/craftdriver/cheatsheet.md +2 -1
  68. package/skills/craftdriver/patterns.md +2 -5
@@ -0,0 +1,505 @@
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>Mouse Actions Playground</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1115;
10
+ --panel: #151925;
11
+ --accent: #4f86ff;
12
+ --success: #18a558;
13
+ --fail: #e63946;
14
+ --text: #e6edf3;
15
+ --muted: #9aa5b1;
16
+ }
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+ html,
21
+ body {
22
+ height: 100%;
23
+ }
24
+ body {
25
+ margin: 0;
26
+ font-family:
27
+ ui-sans-serif,
28
+ system-ui,
29
+ -apple-system,
30
+ Segoe UI,
31
+ Roboto,
32
+ Ubuntu,
33
+ Cantarell,
34
+ Noto Sans,
35
+ Helvetica Neue,
36
+ Arial,
37
+ 'Apple Color Emoji',
38
+ 'Segoe UI Emoji';
39
+ background: var(--bg);
40
+ color: var(--text);
41
+ display: grid;
42
+ grid-template-columns: 1fr 360px;
43
+ grid-template-rows: auto 1fr;
44
+ gap: 12px;
45
+ padding: 12px;
46
+ }
47
+ header {
48
+ grid-column: 1 / -1;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 12px;
52
+ }
53
+ header h1 {
54
+ font-size: 18px;
55
+ margin: 0;
56
+ font-weight: 600;
57
+ }
58
+ header .hint {
59
+ color: var(--muted);
60
+ font-size: 12px;
61
+ }
62
+
63
+ .board {
64
+ background: var(--panel);
65
+ border: 1px solid #222839;
66
+ border-radius: 10px;
67
+ padding: 12px;
68
+ position: relative;
69
+ overflow: hidden;
70
+ min-height: 520px;
71
+ }
72
+ .board .grid {
73
+ position: absolute;
74
+ inset: 0;
75
+ pointer-events: none;
76
+ opacity: 0.15;
77
+ background-image:
78
+ linear-gradient(#fff2 1px, transparent 1px),
79
+ linear-gradient(90deg, #fff2 1px, transparent 1px);
80
+ background-size: 24px 24px;
81
+ }
82
+ .row {
83
+ display: grid;
84
+ grid-template-columns: repeat(2, minmax(0, 1fr));
85
+ gap: 12px;
86
+ }
87
+ .card {
88
+ background: #0e1320;
89
+ border: 1px solid #1e263a;
90
+ border-radius: 10px;
91
+ padding: 10px;
92
+ }
93
+ .card h2 {
94
+ font-size: 14px;
95
+ margin: 0 0 8px;
96
+ font-weight: 600;
97
+ }
98
+ .sep {
99
+ height: 1px;
100
+ background: #1e263a;
101
+ margin: 8px 0;
102
+ }
103
+ .pill {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: 6px;
107
+ padding: 4px 8px;
108
+ border-radius: 999px;
109
+ background: #101628;
110
+ border: 1px solid #1f2740;
111
+ color: var(--muted);
112
+ font-size: 12px;
113
+ }
114
+ button {
115
+ background: #14203a;
116
+ color: var(--text);
117
+ border: 1px solid #243257;
118
+ border-radius: 6px;
119
+ padding: 6px 10px;
120
+ cursor: pointer;
121
+ }
122
+ button:hover {
123
+ background: #19294a;
124
+ }
125
+
126
+ /* Drag & Drop area */
127
+ .stage {
128
+ position: relative;
129
+ height: 260px;
130
+ background: #0b1120;
131
+ border: 1px dashed #2a3555;
132
+ border-radius: 8px;
133
+ overflow: hidden;
134
+ }
135
+ #drag-source {
136
+ position: absolute;
137
+ left: 24px;
138
+ top: 24px;
139
+ width: 72px;
140
+ height: 72px;
141
+ border-radius: 10px;
142
+ background: linear-gradient(145deg, #5a7aff, #3e5be8);
143
+ border: 1px solid #2f49c0;
144
+ box-shadow: 0 6px 16px #3e5be880;
145
+ display: grid;
146
+ place-items: center;
147
+ font-weight: 600;
148
+ user-select: none;
149
+ touch-action: none;
150
+ color: #fff;
151
+ }
152
+ #drag-target {
153
+ position: absolute;
154
+ right: 24px;
155
+ bottom: 24px;
156
+ width: 160px;
157
+ height: 120px;
158
+ border-radius: 12px;
159
+ border: 2px dashed #3b4f7d;
160
+ background: #0f1730;
161
+ color: var(--muted);
162
+ display: grid;
163
+ place-items: center;
164
+ text-align: center;
165
+ padding: 8px;
166
+ }
167
+ #drag-target.success {
168
+ border-color: var(--success);
169
+ color: var(--success);
170
+ box-shadow: 0 0 0 2px #18a55833 inset;
171
+ }
172
+ #drag-target.fail {
173
+ border-color: var(--fail);
174
+ color: var(--fail);
175
+ box-shadow: 0 0 0 2px #e6394633 inset;
176
+ }
177
+
178
+ /* Interaction boxes */
179
+ .box {
180
+ height: 90px;
181
+ border-radius: 8px;
182
+ border: 1px solid #28314a;
183
+ background: #0c1426;
184
+ display: grid;
185
+ place-items: center;
186
+ text-align: center;
187
+ padding: 8px;
188
+ user-select: none;
189
+ }
190
+ .box strong {
191
+ color: #c7d2fe;
192
+ }
193
+ #wheel-box {
194
+ height: 240px;
195
+ overflow: auto;
196
+ }
197
+ #wheel-content {
198
+ height: 600px;
199
+ width: 100%;
200
+ background: linear-gradient(#0c1426, #0f172a);
201
+ display: grid;
202
+ place-items: center;
203
+ color: var(--muted);
204
+ }
205
+ #hover-tip {
206
+ display: none;
207
+ position: absolute;
208
+ transform: translateY(60px);
209
+ padding: 6px 10px;
210
+ font-size: 12px;
211
+ background: #111a2e;
212
+ border: 1px solid #233154;
213
+ border-radius: 6px;
214
+ color: var(--muted);
215
+ }
216
+
217
+ /* Sidebar */
218
+ .sidebar {
219
+ background: var(--panel);
220
+ border: 1px solid #222839;
221
+ border-radius: 10px;
222
+ padding: 10px;
223
+ display: grid;
224
+ grid-template-rows: auto auto 1fr;
225
+ gap: 10px;
226
+ }
227
+ .log {
228
+ background: #0b1120;
229
+ border: 1px solid #1b2340;
230
+ border-radius: 8px;
231
+ padding: 8px;
232
+ height: 320px;
233
+ overflow: auto;
234
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
235
+ font-size: 12px;
236
+ }
237
+ .log .item {
238
+ color: #d1d5db;
239
+ }
240
+ .log .time {
241
+ color: #7aa2ff;
242
+ }
243
+ .status {
244
+ display: grid;
245
+ grid-template-columns: auto 1fr;
246
+ gap: 6px 10px;
247
+ align-items: center;
248
+ }
249
+ .status .value {
250
+ font-weight: 600;
251
+ }
252
+ .ok {
253
+ color: var(--success);
254
+ }
255
+ .bad {
256
+ color: var(--fail);
257
+ }
258
+ .coords {
259
+ color: var(--muted);
260
+ font-size: 12px;
261
+ }
262
+ </style>
263
+ </head>
264
+ <body>
265
+ <header>
266
+ <h1>Mouse Actions Playground</h1>
267
+ <span class="hint"
268
+ >Try press, release, move, click, double-click, right-click, wheel, and drag & drop.</span
269
+ >
270
+ <span class="pill" id="coords">x: 0, y: 0</span>
271
+ <button id="reset-btn" title="Reset positions and log">Reset</button>
272
+ </header>
273
+
274
+ <main class="board" id="board">
275
+ <div class="grid"></div>
276
+
277
+ <section class="card">
278
+ <h2>Drag & Drop (success when fully inside the target)</h2>
279
+ <div class="stage" id="stage">
280
+ <div id="drag-source" aria-label="drag-source">DRAG</div>
281
+ <div id="drag-target" aria-label="drag-target">
282
+ Drop here
283
+ <div style="font-size: 11px; color: var(--muted)">needs to be fully inside</div>
284
+ </div>
285
+ </div>
286
+ </section>
287
+
288
+ <div class="row" style="margin-top: 12px">
289
+ <section class="card">
290
+ <h2>Press & Release</h2>
291
+ <div class="box" id="press-box">
292
+ <div><strong>press-box</strong><br /><span>pointerdown / pointerup</span></div>
293
+ </div>
294
+ </section>
295
+ <section class="card">
296
+ <h2>Double click</h2>
297
+ <div class="box" id="dblclick-box">
298
+ <div>
299
+ <strong>dblclick-box</strong><br /><span
300
+ >dblclick counter: <span id="dblcount">0</span></span
301
+ >
302
+ </div>
303
+ </div>
304
+ </section>
305
+ </div>
306
+
307
+ <div class="row" style="margin-top: 12px">
308
+ <section class="card">
309
+ <h2>Right click / Context menu</h2>
310
+ <div class="box" id="context-box">
311
+ <div><strong>context-box</strong><br /><span>right-click here</span></div>
312
+ </div>
313
+ </section>
314
+ <section class="card" style="position: relative">
315
+ <h2>Hover</h2>
316
+ <div class="box" id="hover-box" style="position: relative">
317
+ <div><strong>hover-box</strong><br /><span>shows a tip on hover</span></div>
318
+ <div id="hover-tip">Hello from hover!</div>
319
+ </div>
320
+ </section>
321
+ </div>
322
+ </main>
323
+
324
+ <aside class="sidebar">
325
+ <div class="status">
326
+ <div>Drag result:</div>
327
+ <div id="status" class="value">n/a</div>
328
+ </div>
329
+ <div class="sep"></div>
330
+ <section class="card">
331
+ <h2>Wheel / Scroll</h2>
332
+ <div class="box" id="wheel-box">
333
+ <div id="wheel-content">Scroll in this box (it overflows)</div>
334
+ </div>
335
+ </section>
336
+ <div id="log" class="log" aria-live="polite"></div>
337
+ </aside>
338
+
339
+ <script>
340
+ (function () {
341
+ const $ = (sel) => document.querySelector(sel);
342
+ const logEl = $('#log');
343
+ const status = $('#status');
344
+
345
+ const coords = $('#coords');
346
+ const stage = $('#stage');
347
+ const src = $('#drag-source');
348
+ const tgt = $('#drag-target');
349
+
350
+ const resetBtn = $('#reset-btn');
351
+
352
+ const fmtTime = () => new Date().toLocaleTimeString();
353
+ function log(msg) {
354
+ const div = document.createElement('div');
355
+ div.className = 'item';
356
+ div.innerHTML = '<span class="time">[' + fmtTime() + ']</span> ' + msg;
357
+ logEl.appendChild(div);
358
+ while (logEl.children.length > 200) logEl.removeChild(logEl.firstChild);
359
+ logEl.scrollTop = logEl.scrollHeight;
360
+ }
361
+
362
+ // coords display
363
+ document.addEventListener(
364
+ 'pointermove',
365
+ (e) => {
366
+ coords.textContent = 'x: ' + Math.round(e.clientX) + ', y: ' + Math.round(e.clientY);
367
+ },
368
+ { passive: true }
369
+ );
370
+
371
+ // Press / Release box
372
+ const pressBox = $('#press-box');
373
+ pressBox.addEventListener('pointerdown', (e) => {
374
+ log('press-box: pointerdown button=' + e.button);
375
+ });
376
+ pressBox.addEventListener('pointerup', (e) => {
377
+ log('press-box: pointerup button=' + e.button);
378
+ });
379
+
380
+ // Double-click box
381
+ const dblBox = $('#dblclick-box');
382
+ const dblCount = $('#dblcount');
383
+ dblBox.addEventListener('dblclick', () => {
384
+ dblCount.textContent = String(Number(dblCount.textContent || '0') + 1);
385
+ log('dblclick-box: dblclick');
386
+ });
387
+
388
+ // Context menu (right-click)
389
+ const ctxBox = $('#context-box');
390
+ ctxBox.addEventListener('contextmenu', (e) => {
391
+ e.preventDefault();
392
+ log('context-box: contextmenu (right click)');
393
+ });
394
+
395
+ // Hover tip
396
+ const hoverBox = $('#hover-box');
397
+ const tip = $('#hover-tip');
398
+ hoverBox.addEventListener('pointerenter', () => {
399
+ tip.style.display = 'block';
400
+ log('hover-box: enter');
401
+ });
402
+ hoverBox.addEventListener('pointerleave', () => {
403
+ tip.style.display = 'none';
404
+ log('hover-box: leave');
405
+ });
406
+
407
+ // Wheel box
408
+ const wheelBox = $('#wheel-box');
409
+ wheelBox.addEventListener(
410
+ 'wheel',
411
+ (e) => {
412
+ log('wheel-box: wheel dx=' + e.deltaX + ' dy=' + e.deltaY);
413
+ },
414
+ { passive: true }
415
+ );
416
+
417
+ // Drag logic (pointer events, works with WebDriver pointer actions)
418
+ let dragging = false;
419
+ let offsetX = 0;
420
+ let offsetY = 0;
421
+ let startPos = { left: 24, top: 24 };
422
+
423
+ function setSrcPos(left, top) {
424
+ src.style.left = left + 'px';
425
+ src.style.top = top + 'px';
426
+ }
427
+ setSrcPos(startPos.left, startPos.top);
428
+
429
+ src.addEventListener('pointerdown', (e) => {
430
+ dragging = true;
431
+ src.setPointerCapture(e.pointerId);
432
+ const r = src.getBoundingClientRect();
433
+ offsetX = e.clientX - r.left;
434
+ offsetY = e.clientY - r.top;
435
+ log('drag: pointerdown at ' + e.clientX + ',' + e.clientY);
436
+ });
437
+
438
+ document.addEventListener('pointermove', (e) => {
439
+ if (!dragging) return;
440
+ const stageR = stage.getBoundingClientRect();
441
+ let left = e.clientX - stageR.left - offsetX;
442
+ let top = e.clientY - stageR.top - offsetY;
443
+ // keep inside stage bounds
444
+ const r = src.getBoundingClientRect();
445
+ const w = r.width,
446
+ h = r.height;
447
+ left = Math.max(0, Math.min(left, stageR.width - w));
448
+ top = Math.max(0, Math.min(top, stageR.height - h));
449
+ setSrcPos(left, top);
450
+ });
451
+
452
+ function isFullyInside(inner, outer) {
453
+ const a = inner.getBoundingClientRect();
454
+ const b = outer.getBoundingClientRect();
455
+ return a.left >= b.left && a.top >= b.top && a.right <= b.right && a.bottom <= b.bottom;
456
+ }
457
+
458
+ function updateDropStatus() {
459
+ const inside1 = isFullyInside(src, tgt);
460
+ status.textContent = inside1 ? 'success' : 'fail';
461
+ status.className = 'value ' + (inside1 ? 'ok' : 'bad');
462
+ tgt.classList.toggle('success', inside1);
463
+ tgt.classList.toggle('fail', !inside1);
464
+ }
465
+
466
+ document.addEventListener('pointerup', (e) => {
467
+ if (!dragging) return;
468
+ dragging = false;
469
+ try {
470
+ src.releasePointerCapture(e.pointerId);
471
+ } catch {}
472
+ updateDropStatus();
473
+ const inside1 = isFullyInside(src, tgt);
474
+ const mods =
475
+ [
476
+ e.shiftKey ? 'Shift' : '',
477
+ e.ctrlKey ? 'Ctrl' : '',
478
+ e.altKey ? 'Alt' : '',
479
+ e.metaKey ? 'Meta' : '',
480
+ ]
481
+ .filter(Boolean)
482
+ .join('+') || 'none';
483
+ log(
484
+ 'drag: drop at ' +
485
+ e.clientX +
486
+ ',' +
487
+ e.clientY +
488
+ ' — target=' +
489
+ (inside1 ? 'success' : 'fail') +
490
+ ', modifiers=' +
491
+ mods
492
+ );
493
+ });
494
+
495
+ resetBtn.addEventListener('click', () => {
496
+ setSrcPos(startPos.left, startPos.top);
497
+ status.textContent = 'n/a';
498
+ status.className = 'value';
499
+ tgt.classList.remove('success', 'fail');
500
+ log('reset');
501
+ });
502
+ })();
503
+ </script>
504
+ </body>
505
+ </html>