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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/cli/snapshot.js +31 -4
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +4 -5
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +18 -8
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +12 -1
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +20 -8
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/browser.d.ts +6 -9
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +6 -4
- 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 -2
- package/docs/browser-logs.md +8 -23
- 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 +27 -39
- package/docs/recipes/file-upload-download.md +24 -44
- package/docs/recipes/find-elements.md +142 -0
- package/docs/recipes/login-once-reuse-session.md +22 -45
- package/docs/recipes/mobile-flow-with-network-and-logs.md +22 -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 +15 -15
- package/docs/recipes.md +20 -1
- package/docs/selectors.md +291 -229
- package/docs/tracing.md +2 -2
- package/package.json +5 -1
- package/skills/craftdriver/SKILL.md +3 -2
- package/skills/craftdriver/cheatsheet.md +2 -1
- package/skills/craftdriver/patterns.md +2 -5
|
@@ -0,0 +1,293 @@
|
|
|
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>Network Calls 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
|
+
button {
|
|
35
|
+
background: #007bff;
|
|
36
|
+
color: white;
|
|
37
|
+
border: none;
|
|
38
|
+
padding: 10px 20px;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
margin-right: 10px;
|
|
42
|
+
margin-bottom: 10px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button:hover {
|
|
46
|
+
background: #0056b3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button:disabled {
|
|
50
|
+
background: #ccc;
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.result {
|
|
55
|
+
background: #f8f9fa;
|
|
56
|
+
border: 1px solid #dee2e6;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
padding: 15px;
|
|
59
|
+
margin-top: 15px;
|
|
60
|
+
min-height: 60px;
|
|
61
|
+
font-family: monospace;
|
|
62
|
+
white-space: pre-wrap;
|
|
63
|
+
word-break: break-all;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.loading {
|
|
67
|
+
color: #666;
|
|
68
|
+
font-style: italic;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.error {
|
|
72
|
+
color: #dc3545;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.success {
|
|
76
|
+
color: #28a745;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
code {
|
|
80
|
+
background: #e9ecef;
|
|
81
|
+
padding: 2px 6px;
|
|
82
|
+
border-radius: 3px;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
|
|
87
|
+
<body>
|
|
88
|
+
<h1>🌐 Network Calls Example</h1>
|
|
89
|
+
<p>This page demonstrates various network requests for testing mocking and interception.</p>
|
|
90
|
+
|
|
91
|
+
<!-- Fetch JSON API -->
|
|
92
|
+
<div class="card">
|
|
93
|
+
<h2>1. Fetch JSON API</h2>
|
|
94
|
+
<p>Fetches data from <code>/api/users</code> endpoint.</p>
|
|
95
|
+
<button id="fetch-users-btn" onclick="fetchUsers()">Fetch Users</button>
|
|
96
|
+
<div id="users-result" class="result">Click button to fetch users...</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- POST Request -->
|
|
100
|
+
<div class="card">
|
|
101
|
+
<h2>2. POST Request</h2>
|
|
102
|
+
<p>Sends a POST request to <code>/api/login</code> with credentials.</p>
|
|
103
|
+
<button id="post-login-btn" onclick="postLogin()">Send Login Request</button>
|
|
104
|
+
<div id="login-result" class="result">Click button to send login request...</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- External API -->
|
|
108
|
+
<div class="card">
|
|
109
|
+
<h2>3. External API Call</h2>
|
|
110
|
+
<p>Fetches data from <code>https://jsonplaceholder.typicode.com/posts/1</code></p>
|
|
111
|
+
<button id="fetch-external-btn" onclick="fetchExternal()">Fetch External Post</button>
|
|
112
|
+
<div id="external-result" class="result">Click button to fetch external data...</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- Multiple Requests -->
|
|
116
|
+
<div class="card">
|
|
117
|
+
<h2>4. Multiple Parallel Requests</h2>
|
|
118
|
+
<p>Makes 3 parallel requests to <code>/api/items/1</code>, <code>/api/items/2</code>, <code>/api/items/3</code></p>
|
|
119
|
+
<button id="fetch-parallel-btn" onclick="fetchParallel()">Fetch All Items</button>
|
|
120
|
+
<div id="parallel-result" class="result">Click button to fetch items in parallel...</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Delayed Response -->
|
|
124
|
+
<div class="card">
|
|
125
|
+
<h2>5. Slow Endpoint</h2>
|
|
126
|
+
<p>Requests <code>/api/slow</code> which simulates a 2-second delay.</p>
|
|
127
|
+
<button id="fetch-slow-btn" onclick="fetchSlow()">Fetch Slow Endpoint</button>
|
|
128
|
+
<div id="slow-result" class="result">Click button to test slow endpoint...</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<script>
|
|
132
|
+
// Check if BiDi testing mode is enabled (no client-side mocking)
|
|
133
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
134
|
+
const bidiMode = urlParams.get('bidi') === 'true';
|
|
135
|
+
|
|
136
|
+
// Simple mock server responses (for standalone testing without BiDi)
|
|
137
|
+
const mockResponses = {
|
|
138
|
+
'/api/users': {
|
|
139
|
+
users: [
|
|
140
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
141
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
142
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
'/api/login': { success: true, token: 'mock-jwt-token-12345' },
|
|
146
|
+
'/api/items/1': { id: 1, name: 'Item One', price: 9.99 },
|
|
147
|
+
'/api/items/2': { id: 2, name: 'Item Two', price: 19.99 },
|
|
148
|
+
'/api/items/3': { id: 3, name: 'Item Three', price: 29.99 },
|
|
149
|
+
'/api/slow': { message: 'This response was delayed by 2 seconds' }
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Intercept fetch to simulate server (only for standalone testing, NOT BiDi mode)
|
|
153
|
+
if (!bidiMode) {
|
|
154
|
+
const originalFetch = window.fetch;
|
|
155
|
+
window.fetch = async function (url, options = {}) {
|
|
156
|
+
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
157
|
+
|
|
158
|
+
// Check if it's a mock endpoint
|
|
159
|
+
for (const [endpoint, response] of Object.entries(mockResponses)) {
|
|
160
|
+
if (urlStr.endsWith(endpoint) || urlStr.includes(endpoint)) {
|
|
161
|
+
// Simulate network delay
|
|
162
|
+
const delay = endpoint === '/api/slow' ? 2000 : 100;
|
|
163
|
+
await new Promise(r => setTimeout(r, delay));
|
|
164
|
+
|
|
165
|
+
return new Response(JSON.stringify(response), {
|
|
166
|
+
status: 200,
|
|
167
|
+
headers: { 'Content-Type': 'application/json' }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// For external URLs, use real fetch
|
|
173
|
+
return originalFetch.apply(this, arguments);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function fetchUsers() {
|
|
178
|
+
const btn = document.getElementById('fetch-users-btn');
|
|
179
|
+
const result = document.getElementById('users-result');
|
|
180
|
+
|
|
181
|
+
btn.disabled = true;
|
|
182
|
+
result.className = 'result loading';
|
|
183
|
+
result.textContent = 'Loading...';
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch('/api/users');
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
result.className = 'result success';
|
|
189
|
+
result.textContent = JSON.stringify(data, null, 2);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
result.className = 'result error';
|
|
192
|
+
result.textContent = 'Error: ' + error.message;
|
|
193
|
+
} finally {
|
|
194
|
+
btn.disabled = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function postLogin() {
|
|
199
|
+
const btn = document.getElementById('post-login-btn');
|
|
200
|
+
const result = document.getElementById('login-result');
|
|
201
|
+
|
|
202
|
+
btn.disabled = true;
|
|
203
|
+
result.className = 'result loading';
|
|
204
|
+
result.textContent = 'Sending login request...';
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch('/api/login', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ username: 'testuser', password: 'secret123' })
|
|
211
|
+
});
|
|
212
|
+
const data = await response.json();
|
|
213
|
+
result.className = 'result success';
|
|
214
|
+
result.textContent = 'Response:\n' + JSON.stringify(data, null, 2);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
result.className = 'result error';
|
|
217
|
+
result.textContent = 'Error: ' + error.message;
|
|
218
|
+
} finally {
|
|
219
|
+
btn.disabled = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function fetchExternal() {
|
|
224
|
+
const btn = document.getElementById('fetch-external-btn');
|
|
225
|
+
const result = document.getElementById('external-result');
|
|
226
|
+
|
|
227
|
+
btn.disabled = true;
|
|
228
|
+
result.className = 'result loading';
|
|
229
|
+
result.textContent = 'Fetching from jsonplaceholder...';
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
|
|
233
|
+
const data = await response.json();
|
|
234
|
+
result.className = 'result success';
|
|
235
|
+
result.textContent = JSON.stringify(data, null, 2);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
result.className = 'result error';
|
|
238
|
+
result.textContent = 'Error: ' + error.message;
|
|
239
|
+
} finally {
|
|
240
|
+
btn.disabled = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function fetchParallel() {
|
|
245
|
+
const btn = document.getElementById('fetch-parallel-btn');
|
|
246
|
+
const result = document.getElementById('parallel-result');
|
|
247
|
+
|
|
248
|
+
btn.disabled = true;
|
|
249
|
+
result.className = 'result loading';
|
|
250
|
+
result.textContent = 'Fetching 3 items in parallel...';
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const [item1, item2, item3] = await Promise.all([
|
|
254
|
+
fetch('/api/items/1').then(r => r.json()),
|
|
255
|
+
fetch('/api/items/2').then(r => r.json()),
|
|
256
|
+
fetch('/api/items/3').then(r => r.json())
|
|
257
|
+
]);
|
|
258
|
+
result.className = 'result success';
|
|
259
|
+
result.textContent = 'All items:\n' + JSON.stringify([item1, item2, item3], null, 2);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
result.className = 'result error';
|
|
262
|
+
result.textContent = 'Error: ' + error.message;
|
|
263
|
+
} finally {
|
|
264
|
+
btn.disabled = false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function fetchSlow() {
|
|
269
|
+
const btn = document.getElementById('fetch-slow-btn');
|
|
270
|
+
const result = document.getElementById('slow-result');
|
|
271
|
+
|
|
272
|
+
btn.disabled = true;
|
|
273
|
+
result.className = 'result loading';
|
|
274
|
+
result.textContent = 'Waiting for slow endpoint (2s delay)...';
|
|
275
|
+
|
|
276
|
+
const startTime = Date.now();
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch('/api/slow');
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
const elapsed = Date.now() - startTime;
|
|
281
|
+
result.className = 'result success';
|
|
282
|
+
result.textContent = `Response (took ${elapsed}ms):\n` + JSON.stringify(data, null, 2);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
result.className = 'result error';
|
|
285
|
+
result.textContent = 'Error: ' + error.message;
|
|
286
|
+
} finally {
|
|
287
|
+
btn.disabled = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
</script>
|
|
291
|
+
</body>
|
|
292
|
+
|
|
293
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
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>Popup Target</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
margin: 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#popup-heading {
|
|
15
|
+
font-size: 1.5rem;
|
|
16
|
+
font-weight: bold;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
|
|
21
|
+
<body>
|
|
22
|
+
<h1 id="popup-heading">Popup Window</h1>
|
|
23
|
+
<p id="popup-content">This is the popup page.</p>
|
|
24
|
+
</body>
|
|
25
|
+
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,50 @@
|
|
|
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 Popup</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
|
+
#popup-result {
|
|
29
|
+
margin-top: 0.5rem;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
|
|
34
|
+
<body>
|
|
35
|
+
<h1>Popup Test Page</h1>
|
|
36
|
+
<button id="open-popup">Open Popup</button>
|
|
37
|
+
<p id="popup-result"></p>
|
|
38
|
+
<script>
|
|
39
|
+
document.getElementById('open-popup').addEventListener('click', function () {
|
|
40
|
+
const w = window.open('/popup-target.html', '_blank', 'width=600,height=400');
|
|
41
|
+
if (!w) {
|
|
42
|
+
document.getElementById('popup-result').textContent = 'blocked';
|
|
43
|
+
} else {
|
|
44
|
+
document.getElementById('popup-result').textContent = 'opened';
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
</body>
|
|
49
|
+
|
|
50
|
+
</html>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>Screenshot Fixture</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: system-ui, sans-serif;
|
|
16
|
+
background: #ffffff;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* A predictable, very tall document so a full-page screenshot
|
|
20
|
+
must be taller than the viewport. Each section is exactly
|
|
21
|
+
600px tall; ten sections = 6000px total height. */
|
|
22
|
+
.section {
|
|
23
|
+
height: 600px;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
font-size: 48px;
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
color: #ffffff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#s1 {
|
|
33
|
+
background: #ef4444;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#s2 {
|
|
37
|
+
background: #f97316;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#s3 {
|
|
41
|
+
background: #eab308;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#s4 {
|
|
45
|
+
background: #22c55e;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#s5 {
|
|
49
|
+
background: #14b8a6;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#s6 {
|
|
53
|
+
background: #0ea5e9;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#s7 {
|
|
57
|
+
background: #6366f1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#s8 {
|
|
61
|
+
background: #a855f7;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#s9 {
|
|
65
|
+
background: #ec4899;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#s10 {
|
|
69
|
+
background: #111827;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#marker-bottom {
|
|
73
|
+
position: absolute;
|
|
74
|
+
bottom: 0;
|
|
75
|
+
width: 100%;
|
|
76
|
+
text-align: center;
|
|
77
|
+
padding: 8px 0;
|
|
78
|
+
background: #000;
|
|
79
|
+
color: #fff;
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
|
|
85
|
+
<body>
|
|
86
|
+
<div id="s1" class="section">Section 1</div>
|
|
87
|
+
<div id="s2" class="section">Section 2</div>
|
|
88
|
+
<div id="s3" class="section">Section 3</div>
|
|
89
|
+
<div id="s4" class="section">Section 4</div>
|
|
90
|
+
<div id="s5" class="section">Section 5</div>
|
|
91
|
+
<div id="s6" class="section">Section 6</div>
|
|
92
|
+
<div id="s7" class="section">Section 7</div>
|
|
93
|
+
<div id="s8" class="section">Section 8</div>
|
|
94
|
+
<div id="s9" class="section">Section 9</div>
|
|
95
|
+
<div id="s10" class="section">Section 10</div>
|
|
96
|
+
<div id="marker-bottom">bottom-of-document</div>
|
|
97
|
+
</body>
|
|
98
|
+
|
|
99
|
+
</html>
|
|
@@ -0,0 +1,150 @@
|
|
|
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>Selectors Playground</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family:
|
|
11
|
+
system-ui,
|
|
12
|
+
-apple-system,
|
|
13
|
+
Segoe UI,
|
|
14
|
+
Roboto,
|
|
15
|
+
Arial;
|
|
16
|
+
margin: 20px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card {
|
|
20
|
+
border: 1px solid #ddd;
|
|
21
|
+
border-radius: 8px;
|
|
22
|
+
padding: 12px;
|
|
23
|
+
margin-bottom: 12px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.row {
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
29
|
+
gap: 12px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.muted {
|
|
33
|
+
color: #6b7280;
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.btn {
|
|
38
|
+
padding: 6px 10px;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
border: 1px solid #d1d5db;
|
|
41
|
+
background: #f9fafb;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
|
|
47
|
+
<body>
|
|
48
|
+
<h1>Selectors Playground</h1>
|
|
49
|
+
|
|
50
|
+
<section class="card" id="area-attrs">
|
|
51
|
+
<h2>Attribute-based</h2>
|
|
52
|
+
<input id="by-id" placeholder="by id" />
|
|
53
|
+
<input id="by-name" name="by-name" placeholder="by name" />
|
|
54
|
+
<div class="foo" id="by-class">class=foo</div>
|
|
55
|
+
<span title="Hint title" id="by-title">has title</span>
|
|
56
|
+
<img id="by-alt" alt="Logo ALT" src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" />
|
|
57
|
+
<div id="by-testid" data-testid="by-testid">data-testid</div>
|
|
58
|
+
<div data-custom="cval" id="by-data-attr">data-custom</div>
|
|
59
|
+
<div aria-label="Aria Name" role="button" id="by-role">button aria</div>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<section class="card" id="area-text">
|
|
63
|
+
<h2>Text-based</h2>
|
|
64
|
+
<div id="text-exact">Exact Match</div>
|
|
65
|
+
<div id="text-partial">Contains Substring Here</div>
|
|
66
|
+
<div id="text-mixed">Spaced Text</div>
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<section class="card" id="area-label">
|
|
70
|
+
<h2>Labels</h2>
|
|
71
|
+
<label for="email">Email Address</label>
|
|
72
|
+
<input id="email" type="email" placeholder="Enter email" />
|
|
73
|
+
<label>Wrapped Label<input id="wrapped" type="text" /></label>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section class="card" id="area-scope">
|
|
77
|
+
<h2>Scoped composition</h2>
|
|
78
|
+
<div id="scope-card-1" class="product-card"><button id="scope-card-1-btn">Submit</button></div>
|
|
79
|
+
<div id="scope-card-2" class="product-card"><button id="scope-card-2-btn">Submit</button></div>
|
|
80
|
+
<div id="scope-card-3" class="product-card"><button id="scope-card-3-btn">Cancel</button></div>
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<section class="card" id="area-roles-extra">
|
|
84
|
+
<h2>Extra roles</h2>
|
|
85
|
+
<select id="single-select">
|
|
86
|
+
<option>One</option>
|
|
87
|
+
<option>Two</option>
|
|
88
|
+
</select>
|
|
89
|
+
<select id="multi-select" multiple>
|
|
90
|
+
<option>One</option>
|
|
91
|
+
<option>Two</option>
|
|
92
|
+
</select>
|
|
93
|
+
<article id="article-with-header"><header id="article-header">Article Header</header></article>
|
|
94
|
+
<article id="article-with-footer"><footer id="article-footer">Article Footer</footer></article>
|
|
95
|
+
</section>
|
|
96
|
+
|
|
97
|
+
<!-- Outside any sectioning content (article/aside/main/nav/section) on purpose:
|
|
98
|
+
a <header>/<footer> here keeps the banner/contentinfo role, unlike the
|
|
99
|
+
ones nested in <article> above. -->
|
|
100
|
+
<header id="page-header">Page Header</header>
|
|
101
|
+
<footer id="page-footer">Page Footer</footer>
|
|
102
|
+
|
|
103
|
+
<section class="card" id="area-links">
|
|
104
|
+
<h2>Links</h2>
|
|
105
|
+
<a id="link-exact" href="#docs">Documentation</a>
|
|
106
|
+
<a id="link-partial" href="#guide">Read the Guide Now</a>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<section class="card" id="area-hidden">
|
|
110
|
+
<h2>Accessibility-hidden</h2>
|
|
111
|
+
<!-- aria-hidden on the wrapper removes the whole subtree from the a11y
|
|
112
|
+
tree; the button is still visually rendered (aria-hidden != display). -->
|
|
113
|
+
<div aria-hidden="true">
|
|
114
|
+
<button id="hidden-btn">Ghost</button>
|
|
115
|
+
</div>
|
|
116
|
+
<button id="visible-btn">Ghost</button>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<section class="card" id="result">
|
|
120
|
+
<h2>Result</h2>
|
|
121
|
+
<div id="status" class="muted">idle</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
(function () {
|
|
126
|
+
const $ = (s) => document.querySelector(s);
|
|
127
|
+
const status = $('#status');
|
|
128
|
+
function mark(id) {
|
|
129
|
+
status.textContent = id;
|
|
130
|
+
}
|
|
131
|
+
// Use a single delegated handler and mark the closest element with an id
|
|
132
|
+
// Stop propagation so ancestors don't override the status with their own ids
|
|
133
|
+
document.addEventListener(
|
|
134
|
+
'click',
|
|
135
|
+
(e) => {
|
|
136
|
+
const target = e.target;
|
|
137
|
+
if (!(target instanceof Element)) return;
|
|
138
|
+
const withId = target.closest('[id]');
|
|
139
|
+
if (withId) {
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
mark(withId.id);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
true
|
|
145
|
+
);
|
|
146
|
+
})();
|
|
147
|
+
</script>
|
|
148
|
+
</body>
|
|
149
|
+
|
|
150
|
+
</html>
|