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,344 @@
|
|
|
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>Console & Errors Example</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
|
+
h3 {
|
|
35
|
+
color: #888;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
button {
|
|
39
|
+
padding: 10px 20px;
|
|
40
|
+
border: none;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
margin-right: 10px;
|
|
44
|
+
margin-bottom: 10px;
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.btn-log {
|
|
49
|
+
background: #17a2b8;
|
|
50
|
+
color: white;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.btn-warn {
|
|
54
|
+
background: #ffc107;
|
|
55
|
+
color: #333;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.btn-error {
|
|
59
|
+
background: #dc3545;
|
|
60
|
+
color: white;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.btn-info {
|
|
64
|
+
background: #6c757d;
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn-debug {
|
|
69
|
+
background: #28a745;
|
|
70
|
+
color: white;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.btn-throw {
|
|
74
|
+
background: #6f42c1;
|
|
75
|
+
color: white;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
button:hover {
|
|
79
|
+
opacity: 0.9;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.note {
|
|
83
|
+
background: #fff3cd;
|
|
84
|
+
border: 1px solid #ffc107;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
padding: 10px;
|
|
87
|
+
margin-top: 15px;
|
|
88
|
+
font-size: 14px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.output {
|
|
92
|
+
background: #1e1e1e;
|
|
93
|
+
color: #d4d4d4;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
padding: 15px;
|
|
96
|
+
margin-top: 15px;
|
|
97
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
98
|
+
font-size: 13px;
|
|
99
|
+
max-height: 200px;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.output .log {
|
|
104
|
+
color: #d4d4d4;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.output .warn {
|
|
108
|
+
color: #dcdcaa;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.output .error {
|
|
112
|
+
color: #f48771;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.output .info {
|
|
116
|
+
color: #9cdcfe;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.output .debug {
|
|
120
|
+
color: #6a9955;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
code {
|
|
124
|
+
background: #e9ecef;
|
|
125
|
+
padding: 2px 6px;
|
|
126
|
+
border-radius: 3px;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
|
|
131
|
+
<body>
|
|
132
|
+
<h1>🖥️ Console & JavaScript Errors</h1>
|
|
133
|
+
<p>This page generates console messages and JavaScript errors for testing BiDi log monitoring.</p>
|
|
134
|
+
|
|
135
|
+
<!-- Console Log Methods -->
|
|
136
|
+
<div class="card">
|
|
137
|
+
<h2>1. Console Log Methods</h2>
|
|
138
|
+
<p>Click buttons to trigger different console methods. Open DevTools (F12) to see output.</p>
|
|
139
|
+
|
|
140
|
+
<button id="btn-console-log" class="btn-log" onclick="doLog()">console.log()</button>
|
|
141
|
+
<button id="btn-console-warn" class="btn-warn" onclick="doWarn()">console.warn()</button>
|
|
142
|
+
<button id="btn-console-error" class="btn-error" onclick="doError()">console.error()</button>
|
|
143
|
+
<button id="btn-console-info" class="btn-info" onclick="doInfo()">console.info()</button>
|
|
144
|
+
<button id="btn-console-debug" class="btn-debug" onclick="doDebug()">console.debug()</button>
|
|
145
|
+
|
|
146
|
+
<div class="note">
|
|
147
|
+
💡 These buttons call <code>console.log()</code>, <code>console.warn()</code>, etc.
|
|
148
|
+
BiDi can capture all these in real-time!
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Console with Objects -->
|
|
153
|
+
<div class="card">
|
|
154
|
+
<h2>2. Complex Console Output</h2>
|
|
155
|
+
<p>Log complex objects, arrays, and multiple arguments.</p>
|
|
156
|
+
|
|
157
|
+
<button id="btn-log-object" class="btn-log" onclick="logObject()">Log Object</button>
|
|
158
|
+
<button id="btn-log-array" class="btn-log" onclick="logArray()">Log Array</button>
|
|
159
|
+
<button id="btn-log-multiple" class="btn-log" onclick="logMultiple()">Log Multiple Args</button>
|
|
160
|
+
<button id="btn-log-formatted" class="btn-log" onclick="logFormatted()">Log Formatted String</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- JavaScript Errors -->
|
|
164
|
+
<div class="card">
|
|
165
|
+
<h2>3. JavaScript Errors (Thrown)</h2>
|
|
166
|
+
<p>Click buttons to throw actual JavaScript errors. BiDi captures these as uncaught exceptions.</p>
|
|
167
|
+
|
|
168
|
+
<button id="btn-throw-error" class="btn-throw" onclick="throwError()">Throw Error</button>
|
|
169
|
+
<button id="btn-throw-type" class="btn-throw" onclick="throwTypeError()">Throw TypeError</button>
|
|
170
|
+
<button id="btn-throw-reference" class="btn-throw" onclick="throwReferenceError()">Throw ReferenceError</button>
|
|
171
|
+
<button id="btn-throw-syntax" class="btn-throw" onclick="throwWithEval()">Eval Syntax Error</button>
|
|
172
|
+
|
|
173
|
+
<div class="note">
|
|
174
|
+
⚠️ These buttons throw <strong>real uncaught exceptions</strong>.
|
|
175
|
+
You'll see them in the console as red errors.
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Promise Rejections -->
|
|
180
|
+
<div class="card">
|
|
181
|
+
<h2>4. Async Errors & Promise Rejections</h2>
|
|
182
|
+
<p>Unhandled promise rejections and async errors.</p>
|
|
183
|
+
|
|
184
|
+
<button id="btn-promise-reject" class="btn-throw" onclick="rejectPromise()">Unhandled Promise Rejection</button>
|
|
185
|
+
<button id="btn-async-error" class="btn-throw" onclick="asyncError()">Async Function Error</button>
|
|
186
|
+
<button id="btn-timeout-error" class="btn-throw" onclick="timeoutError()">Error in setTimeout</button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Triggered Errors -->
|
|
190
|
+
<div class="card">
|
|
191
|
+
<h2>5. DOM Errors</h2>
|
|
192
|
+
<p>Errors triggered by DOM operations.</p>
|
|
193
|
+
|
|
194
|
+
<button id="btn-null-access" class="btn-throw" onclick="nullAccess()">Access Null Element</button>
|
|
195
|
+
<button id="btn-invalid-selector" class="btn-throw" onclick="invalidSelector()">Invalid Selector</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Log Display -->
|
|
199
|
+
<div class="card">
|
|
200
|
+
<h2>📋 Local Log Display</h2>
|
|
201
|
+
<p>Shows logs captured on this page (for visual reference):</p>
|
|
202
|
+
<div id="log-output" class="output">Logs will appear here...</div>
|
|
203
|
+
<button onclick="clearLogs()" style="margin-top: 10px; background: #6c757d; color: white;">Clear Logs</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<script>
|
|
207
|
+
const logOutput = document.getElementById('log-output');
|
|
208
|
+
let logCount = 0;
|
|
209
|
+
|
|
210
|
+
function appendToDisplay(type, message) {
|
|
211
|
+
logCount++;
|
|
212
|
+
const line = document.createElement('div');
|
|
213
|
+
line.className = type;
|
|
214
|
+
line.textContent = `[${logCount}] [${type.toUpperCase()}] ${message}`;
|
|
215
|
+
logOutput.appendChild(line);
|
|
216
|
+
logOutput.scrollTop = logOutput.scrollHeight;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function clearLogs() {
|
|
220
|
+
logOutput.innerHTML = '';
|
|
221
|
+
logCount = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Console methods
|
|
225
|
+
function doLog() {
|
|
226
|
+
const msg = 'Hello from console.log!';
|
|
227
|
+
console.log(msg);
|
|
228
|
+
appendToDisplay('log', msg);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function doWarn() {
|
|
232
|
+
const msg = 'This is a warning message';
|
|
233
|
+
console.warn(msg);
|
|
234
|
+
appendToDisplay('warn', msg);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function doError() {
|
|
238
|
+
const msg = 'This is an error message (via console.error)';
|
|
239
|
+
console.error(msg);
|
|
240
|
+
appendToDisplay('error', msg);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function doInfo() {
|
|
244
|
+
const msg = 'This is an info message';
|
|
245
|
+
console.info(msg);
|
|
246
|
+
appendToDisplay('info', msg);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function doDebug() {
|
|
250
|
+
const msg = 'This is a debug message';
|
|
251
|
+
console.debug(msg);
|
|
252
|
+
appendToDisplay('debug', msg);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Complex logging
|
|
256
|
+
function logObject() {
|
|
257
|
+
const obj = {
|
|
258
|
+
user: 'testuser',
|
|
259
|
+
role: 'admin',
|
|
260
|
+
permissions: ['read', 'write', 'delete'],
|
|
261
|
+
metadata: { created: new Date().toISOString() }
|
|
262
|
+
};
|
|
263
|
+
console.log('User object:', obj);
|
|
264
|
+
appendToDisplay('log', 'User object: ' + JSON.stringify(obj));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function logArray() {
|
|
268
|
+
const arr = [1, 2, 3, { nested: true }, ['a', 'b', 'c']];
|
|
269
|
+
console.log('Array:', arr);
|
|
270
|
+
appendToDisplay('log', 'Array: ' + JSON.stringify(arr));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function logMultiple() {
|
|
274
|
+
console.log('Multiple', 'arguments', 123, true, { key: 'value' });
|
|
275
|
+
appendToDisplay('log', 'Multiple arguments 123 true {"key":"value"}');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function logFormatted() {
|
|
279
|
+
console.log('User %s has %d items (verified: %o)', 'Alice', 5, true);
|
|
280
|
+
appendToDisplay('log', 'User Alice has 5 items (verified: true)');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// JavaScript errors
|
|
284
|
+
function throwError() {
|
|
285
|
+
appendToDisplay('error', 'About to throw Error...');
|
|
286
|
+
throw new Error('This is a thrown Error!');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function throwTypeError() {
|
|
290
|
+
appendToDisplay('error', 'About to throw TypeError...');
|
|
291
|
+
throw new TypeError('Cannot read property "foo" of undefined');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function throwReferenceError() {
|
|
295
|
+
appendToDisplay('error', 'About to throw ReferenceError...');
|
|
296
|
+
throw new ReferenceError('undefinedVariable is not defined');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function throwWithEval() {
|
|
300
|
+
appendToDisplay('error', 'About to eval invalid syntax...');
|
|
301
|
+
eval('function() { invalid syntax here }}}');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Async errors
|
|
305
|
+
function rejectPromise() {
|
|
306
|
+
appendToDisplay('error', 'Creating unhandled promise rejection...');
|
|
307
|
+
new Promise((_, reject) => {
|
|
308
|
+
reject(new Error('Unhandled promise rejection!'));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function asyncError() {
|
|
313
|
+
appendToDisplay('error', 'Calling async function that throws...');
|
|
314
|
+
(async () => {
|
|
315
|
+
throw new Error('Error inside async function!');
|
|
316
|
+
})();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function timeoutError() {
|
|
320
|
+
appendToDisplay('error', 'Error will occur in 100ms...');
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
throw new Error('Error inside setTimeout!');
|
|
323
|
+
}, 100);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// DOM errors
|
|
327
|
+
function nullAccess() {
|
|
328
|
+
appendToDisplay('error', 'Accessing property of null element...');
|
|
329
|
+
const elem = document.getElementById('non-existent-element');
|
|
330
|
+
elem.textContent = 'This will fail';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function invalidSelector() {
|
|
334
|
+
appendToDisplay('error', 'Using invalid CSS selector...');
|
|
335
|
+
document.querySelector('div[invalid==>]');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Initial log
|
|
339
|
+
console.log('Page loaded: console-errors.html');
|
|
340
|
+
appendToDisplay('log', 'Page loaded: console-errors.html');
|
|
341
|
+
</script>
|
|
342
|
+
</body>
|
|
343
|
+
|
|
344
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>Dialogs</title>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Dialogs</h1>
|
|
11
|
+
|
|
12
|
+
<!-- alert -->
|
|
13
|
+
<button id="show-alert">Show Alert</button>
|
|
14
|
+
|
|
15
|
+
<!-- confirm — records whether user accepted or dismissed -->
|
|
16
|
+
<button id="show-confirm">Show Confirm</button>
|
|
17
|
+
<p id="confirm-result"></p>
|
|
18
|
+
|
|
19
|
+
<!-- prompt — echoes the entered value -->
|
|
20
|
+
<button id="show-prompt">Show Prompt</button>
|
|
21
|
+
<p id="prompt-result"></p>
|
|
22
|
+
|
|
23
|
+
<!-- beforeunload is not tested here — browser behaviour varies -->
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
document.getElementById('show-alert').addEventListener('click', () => {
|
|
27
|
+
alert('Hello from alert');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
document.getElementById('show-confirm').addEventListener('click', () => {
|
|
31
|
+
const ok = confirm('Are you sure?');
|
|
32
|
+
document.getElementById('confirm-result').textContent = ok ? 'confirmed' : 'dismissed';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
document.getElementById('show-prompt').addEventListener('click', () => {
|
|
36
|
+
const value = prompt('Enter a value', 'default');
|
|
37
|
+
document.getElementById('prompt-result').textContent = value ?? 'dismissed';
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
</body>
|
|
41
|
+
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>File Download Example</title>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>File Download</h1>
|
|
11
|
+
|
|
12
|
+
<!-- clicking this triggers a download of a small text file -->
|
|
13
|
+
<button id="download-btn" onclick="downloadFile()">Download report.txt</button>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
function downloadFile() {
|
|
17
|
+
const content = 'craftdriver download test\nline 2\nline 3\n';
|
|
18
|
+
const blob = new Blob([content], { type: 'text/plain' });
|
|
19
|
+
const url = URL.createObjectURL(blob);
|
|
20
|
+
const a = document.createElement('a');
|
|
21
|
+
a.href = url;
|
|
22
|
+
a.download = 'report.txt';
|
|
23
|
+
document.body.appendChild(a);
|
|
24
|
+
a.click();
|
|
25
|
+
document.body.removeChild(a);
|
|
26
|
+
URL.revokeObjectURL(url);
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,131 @@
|
|
|
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 Dynamic Elements</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, Arial, sans-serif;
|
|
11
|
+
margin: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.container {
|
|
15
|
+
max-width: 1024px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.grid {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
22
|
+
gap: 16px;
|
|
23
|
+
align-items: start;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.card {
|
|
27
|
+
border: 1px solid #ddd;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
padding: 16px;
|
|
30
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h2 {
|
|
34
|
+
font-size: 1.1rem;
|
|
35
|
+
margin: 0 0 8px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
button {
|
|
39
|
+
margin-top: 8px;
|
|
40
|
+
padding: 8px 12px;
|
|
41
|
+
border: none;
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
background: #2563eb;
|
|
44
|
+
color: #fff;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button:hover {
|
|
50
|
+
background: #1d4ed8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.muted {
|
|
54
|
+
color: #666;
|
|
55
|
+
font-size: 0.9rem;
|
|
56
|
+
margin: 0;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
|
|
61
|
+
<body>
|
|
62
|
+
<div class="container">
|
|
63
|
+
<div class="grid">
|
|
64
|
+
<div class="card">
|
|
65
|
+
<h2>1) Appear after 1s (not in DOM)</h2>
|
|
66
|
+
<p class="muted">Click the button; element will be created after ~1s.</p>
|
|
67
|
+
<button id="btn-appear" type="button">Create element</button>
|
|
68
|
+
<div id="appear-container" style="margin-top: 12px"></div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="card">
|
|
72
|
+
<h2>2) Hidden → Visible after 1s</h2>
|
|
73
|
+
<p class="muted">Element exists in DOM but is hidden and becomes visible.</p>
|
|
74
|
+
<button id="btn-unhide" type="button">Unhide after 1s</button>
|
|
75
|
+
<div style="margin-top: 12px">
|
|
76
|
+
<div id="hidden-el" style="display: none">I was hidden</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="card">
|
|
81
|
+
<h2>3) Visible → Removed (immediately)</h2>
|
|
82
|
+
<p class="muted">Click the button to remove the element from DOM right away.</p>
|
|
83
|
+
<button id="btn-remove" type="button">Remove now</button>
|
|
84
|
+
<div style="margin-top: 12px">
|
|
85
|
+
<div id="to-remove">I will be removed</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="card">
|
|
90
|
+
<h2>4) Visible → Hidden (immediately)</h2>
|
|
91
|
+
<p class="muted">Click the button to hide the element (display:none).</p>
|
|
92
|
+
<button id="btn-hide" type="button">Hide now</button>
|
|
93
|
+
<div style="margin-top: 12px">
|
|
94
|
+
<div id="to-hide">I will be hidden</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
document.getElementById('btn-appear').addEventListener('click', () => {
|
|
102
|
+
const container = document.getElementById('appear-container');
|
|
103
|
+
container.textContent = ''; // clear previous runs
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
const el = document.createElement('div');
|
|
106
|
+
el.id = 'delayed-greeting';
|
|
107
|
+
el.textContent = 'Hello after 1s';
|
|
108
|
+
container.appendChild(el);
|
|
109
|
+
}, 1000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
document.getElementById('btn-unhide').addEventListener('click', () => {
|
|
113
|
+
const el = document.getElementById('hidden-el');
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
el.style.display = 'block';
|
|
116
|
+
}, 1000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
document.getElementById('btn-remove').addEventListener('click', () => {
|
|
120
|
+
const el = document.getElementById('to-remove');
|
|
121
|
+
if (el) el.remove();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
document.getElementById('btn-hide').addEventListener('click', () => {
|
|
125
|
+
const el = document.getElementById('to-hide');
|
|
126
|
+
if (el) el.style.display = 'none';
|
|
127
|
+
});
|
|
128
|
+
</script>
|
|
129
|
+
</body>
|
|
130
|
+
|
|
131
|
+
</html>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Emulation fixture</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: system-ui, sans-serif; margin: 20px; background: #fff; color: #111; }
|
|
8
|
+
#scheme-readout { padding: 8px; border: 1px solid #ccc; }
|
|
9
|
+
/* prefers-color-scheme observable via CSS */
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
body { background: #111; color: #eee; }
|
|
12
|
+
#scheme-readout { border-color: #555; }
|
|
13
|
+
}
|
|
14
|
+
/* prefers-reduced-motion observable via CSS */
|
|
15
|
+
.spinner { animation: spin 1s linear infinite; display: inline-block; }
|
|
16
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
17
|
+
@media (prefers-reduced-motion: reduce) {
|
|
18
|
+
.spinner { animation: none; }
|
|
19
|
+
}
|
|
20
|
+
/* forced-colors observable via CSS */
|
|
21
|
+
#fc-readout { background: canvas; color: canvastext; padding: 8px; }
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<h1>Emulation fixture</h1>
|
|
26
|
+
|
|
27
|
+
<!-- prefers-color-scheme -->
|
|
28
|
+
<section>
|
|
29
|
+
<h2>Color scheme</h2>
|
|
30
|
+
<div id="scheme-readout">readout</div>
|
|
31
|
+
<script>
|
|
32
|
+
(function () {
|
|
33
|
+
var el = document.getElementById('scheme-readout');
|
|
34
|
+
function update() {
|
|
35
|
+
var dark = matchMedia('(prefers-color-scheme: dark)').matches;
|
|
36
|
+
var light = matchMedia('(prefers-color-scheme: light)').matches;
|
|
37
|
+
el.textContent = dark ? 'dark' : light ? 'light' : 'no-preference';
|
|
38
|
+
}
|
|
39
|
+
update();
|
|
40
|
+
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', update);
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<!-- prefers-reduced-motion -->
|
|
46
|
+
<section>
|
|
47
|
+
<h2>Reduced motion</h2>
|
|
48
|
+
<span id="spinner" class="spinner">*</span>
|
|
49
|
+
<div id="rm-readout"></div>
|
|
50
|
+
<script>
|
|
51
|
+
(function () {
|
|
52
|
+
var el = document.getElementById('rm-readout');
|
|
53
|
+
function update() {
|
|
54
|
+
el.textContent = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
55
|
+
? 'reduce' : 'no-preference';
|
|
56
|
+
}
|
|
57
|
+
update();
|
|
58
|
+
matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', update);
|
|
59
|
+
})();
|
|
60
|
+
</script>
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<!-- forced-colors -->
|
|
64
|
+
<section>
|
|
65
|
+
<h2>Forced colors</h2>
|
|
66
|
+
<div id="fc-readout"></div>
|
|
67
|
+
<script>
|
|
68
|
+
(function () {
|
|
69
|
+
var el = document.getElementById('fc-readout');
|
|
70
|
+
function update() {
|
|
71
|
+
el.textContent = matchMedia('(forced-colors: active)').matches
|
|
72
|
+
? 'active' : 'none';
|
|
73
|
+
}
|
|
74
|
+
update();
|
|
75
|
+
})();
|
|
76
|
+
</script>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<!-- locale -->
|
|
80
|
+
<section>
|
|
81
|
+
<h2>Locale</h2>
|
|
82
|
+
<div id="lang-readout"></div>
|
|
83
|
+
<div id="number-readout"></div>
|
|
84
|
+
<script>
|
|
85
|
+
document.getElementById('lang-readout').textContent = navigator.language;
|
|
86
|
+
// 1234.5 in en-US -> "1,234.5", in de-DE -> "1.234,5"
|
|
87
|
+
document.getElementById('number-readout').textContent =
|
|
88
|
+
new Intl.NumberFormat().format(1234.5);
|
|
89
|
+
</script>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<!-- timezone -->
|
|
93
|
+
<section>
|
|
94
|
+
<h2>Timezone</h2>
|
|
95
|
+
<div id="tz-readout"></div>
|
|
96
|
+
<script>
|
|
97
|
+
// Resolved IANA zone, e.g. "Europe/Berlin"
|
|
98
|
+
document.getElementById('tz-readout').textContent =
|
|
99
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
100
|
+
</script>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<!-- offline -->
|
|
104
|
+
<section>
|
|
105
|
+
<h2>Network</h2>
|
|
106
|
+
<div id="online-readout"></div>
|
|
107
|
+
<button id="fetch-btn" type="button">fetch /</button>
|
|
108
|
+
<div id="fetch-readout"></div>
|
|
109
|
+
<script>
|
|
110
|
+
var online = document.getElementById('online-readout');
|
|
111
|
+
function syncOnline() { online.textContent = navigator.onLine ? 'online' : 'offline'; }
|
|
112
|
+
syncOnline();
|
|
113
|
+
window.addEventListener('online', syncOnline);
|
|
114
|
+
window.addEventListener('offline', syncOnline);
|
|
115
|
+
|
|
116
|
+
document.getElementById('fetch-btn').addEventListener('click', function () {
|
|
117
|
+
var out = document.getElementById('fetch-readout');
|
|
118
|
+
out.textContent = '';
|
|
119
|
+
fetch('/').then(
|
|
120
|
+
function (r) { out.textContent = 'ok:' + r.status; },
|
|
121
|
+
function (e) { out.textContent = 'error:' + (e && e.message || e); }
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
</script>
|
|
125
|
+
</section>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|