browser-commander 0.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- package/tests/unit/utilities/wait.test.js +207 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
function App() {
|
|
4
|
+
// Form state
|
|
5
|
+
const [formData, setFormData] = useState({
|
|
6
|
+
name: '',
|
|
7
|
+
email: '',
|
|
8
|
+
password: '',
|
|
9
|
+
bio: '',
|
|
10
|
+
gender: '',
|
|
11
|
+
interests: [],
|
|
12
|
+
country: '',
|
|
13
|
+
newsletter: false,
|
|
14
|
+
terms: false,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const [submitResult, setSubmitResult] = useState(null);
|
|
18
|
+
const [counter, setCounter] = useState(0);
|
|
19
|
+
const [toggleEnabled, setToggleEnabled] = useState(false);
|
|
20
|
+
const [showModal, setShowModal] = useState(false);
|
|
21
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
22
|
+
const [selectedOption, setSelectedOption] = useState('');
|
|
23
|
+
const [dynamicContent, setDynamicContent] = useState(null);
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
|
|
26
|
+
const handleInputChange = (e) => {
|
|
27
|
+
const { name, value, type, checked } = e.target;
|
|
28
|
+
|
|
29
|
+
if (type === 'checkbox' && name === 'interests') {
|
|
30
|
+
setFormData((prev) => ({
|
|
31
|
+
...prev,
|
|
32
|
+
interests: checked
|
|
33
|
+
? [...prev.interests, value]
|
|
34
|
+
: prev.interests.filter((i) => i !== value),
|
|
35
|
+
}));
|
|
36
|
+
} else if (type === 'checkbox') {
|
|
37
|
+
setFormData((prev) => ({ ...prev, [name]: checked }));
|
|
38
|
+
} else {
|
|
39
|
+
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleSubmit = (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setIsLoading(true);
|
|
46
|
+
|
|
47
|
+
// Simulate API call
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
setSubmitResult({
|
|
50
|
+
success: true,
|
|
51
|
+
message: 'Form submitted successfully!',
|
|
52
|
+
data: formData,
|
|
53
|
+
});
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
}, 500);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleReset = () => {
|
|
59
|
+
setFormData({
|
|
60
|
+
name: '',
|
|
61
|
+
email: '',
|
|
62
|
+
password: '',
|
|
63
|
+
bio: '',
|
|
64
|
+
gender: '',
|
|
65
|
+
interests: [],
|
|
66
|
+
country: '',
|
|
67
|
+
newsletter: false,
|
|
68
|
+
terms: false,
|
|
69
|
+
});
|
|
70
|
+
setSubmitResult(null);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const loadDynamicContent = () => {
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
setDynamicContent({
|
|
77
|
+
title: 'Dynamic Content Loaded',
|
|
78
|
+
items: ['Item 1', 'Item 2', 'Item 3'],
|
|
79
|
+
});
|
|
80
|
+
setIsLoading(false);
|
|
81
|
+
}, 300);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="app">
|
|
86
|
+
<h1 data-testid="page-title">React Test App</h1>
|
|
87
|
+
<p>This app is designed for E2E testing with browser-commander</p>
|
|
88
|
+
|
|
89
|
+
{/* Basic Form Section */}
|
|
90
|
+
<section className="section" data-testid="form-section">
|
|
91
|
+
<h2>Contact Form</h2>
|
|
92
|
+
<form onSubmit={handleSubmit} data-testid="contact-form">
|
|
93
|
+
<div className="form-group">
|
|
94
|
+
<label htmlFor="name">Full Name</label>
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
id="name"
|
|
98
|
+
name="name"
|
|
99
|
+
value={formData.name}
|
|
100
|
+
onChange={handleInputChange}
|
|
101
|
+
placeholder="Enter your name"
|
|
102
|
+
data-testid="input-name"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="form-group">
|
|
107
|
+
<label htmlFor="email">Email Address</label>
|
|
108
|
+
<input
|
|
109
|
+
type="email"
|
|
110
|
+
id="email"
|
|
111
|
+
name="email"
|
|
112
|
+
value={formData.email}
|
|
113
|
+
onChange={handleInputChange}
|
|
114
|
+
placeholder="Enter your email"
|
|
115
|
+
data-testid="input-email"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="form-group">
|
|
120
|
+
<label htmlFor="password">Password</label>
|
|
121
|
+
<input
|
|
122
|
+
type="password"
|
|
123
|
+
id="password"
|
|
124
|
+
name="password"
|
|
125
|
+
value={formData.password}
|
|
126
|
+
onChange={handleInputChange}
|
|
127
|
+
placeholder="Enter password"
|
|
128
|
+
data-testid="input-password"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="form-group">
|
|
133
|
+
<label htmlFor="bio">Bio / Description</label>
|
|
134
|
+
<textarea
|
|
135
|
+
id="bio"
|
|
136
|
+
name="bio"
|
|
137
|
+
value={formData.bio}
|
|
138
|
+
onChange={handleInputChange}
|
|
139
|
+
placeholder="Tell us about yourself..."
|
|
140
|
+
data-testid="textarea-bio"
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="form-group">
|
|
145
|
+
<label>Gender</label>
|
|
146
|
+
<div className="radio-group" data-testid="radio-gender">
|
|
147
|
+
{['male', 'female', 'other'].map((gender) => (
|
|
148
|
+
<div className="radio-item" key={gender}>
|
|
149
|
+
<input
|
|
150
|
+
type="radio"
|
|
151
|
+
id={`gender-${gender}`}
|
|
152
|
+
name="gender"
|
|
153
|
+
value={gender}
|
|
154
|
+
checked={formData.gender === gender}
|
|
155
|
+
onChange={handleInputChange}
|
|
156
|
+
data-testid={`radio-${gender}`}
|
|
157
|
+
/>
|
|
158
|
+
<label htmlFor={`gender-${gender}`}>
|
|
159
|
+
{gender.charAt(0).toUpperCase() + gender.slice(1)}
|
|
160
|
+
</label>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="form-group">
|
|
167
|
+
<label>Interests</label>
|
|
168
|
+
<div className="checkbox-group" data-testid="checkbox-interests">
|
|
169
|
+
{['technology', 'sports', 'music', 'travel', 'food'].map(
|
|
170
|
+
(interest) => (
|
|
171
|
+
<div className="checkbox-item" key={interest}>
|
|
172
|
+
<input
|
|
173
|
+
type="checkbox"
|
|
174
|
+
id={`interest-${interest}`}
|
|
175
|
+
name="interests"
|
|
176
|
+
value={interest}
|
|
177
|
+
checked={formData.interests.includes(interest)}
|
|
178
|
+
onChange={handleInputChange}
|
|
179
|
+
data-testid={`checkbox-${interest}`}
|
|
180
|
+
/>
|
|
181
|
+
<label htmlFor={`interest-${interest}`}>
|
|
182
|
+
{interest.charAt(0).toUpperCase() + interest.slice(1)}
|
|
183
|
+
</label>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="form-group">
|
|
191
|
+
<label htmlFor="country">Country</label>
|
|
192
|
+
<select
|
|
193
|
+
id="country"
|
|
194
|
+
name="country"
|
|
195
|
+
value={formData.country}
|
|
196
|
+
onChange={handleInputChange}
|
|
197
|
+
data-testid="select-country"
|
|
198
|
+
>
|
|
199
|
+
<option value="">Select a country</option>
|
|
200
|
+
<option value="us">United States</option>
|
|
201
|
+
<option value="uk">United Kingdom</option>
|
|
202
|
+
<option value="ca">Canada</option>
|
|
203
|
+
<option value="au">Australia</option>
|
|
204
|
+
<option value="de">Germany</option>
|
|
205
|
+
</select>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="form-group">
|
|
209
|
+
<div className="checkbox-item">
|
|
210
|
+
<input
|
|
211
|
+
type="checkbox"
|
|
212
|
+
id="newsletter"
|
|
213
|
+
name="newsletter"
|
|
214
|
+
checked={formData.newsletter}
|
|
215
|
+
onChange={handleInputChange}
|
|
216
|
+
data-testid="checkbox-newsletter"
|
|
217
|
+
/>
|
|
218
|
+
<label htmlFor="newsletter">Subscribe to newsletter</label>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="form-group">
|
|
223
|
+
<div className="checkbox-item">
|
|
224
|
+
<input
|
|
225
|
+
type="checkbox"
|
|
226
|
+
id="terms"
|
|
227
|
+
name="terms"
|
|
228
|
+
checked={formData.terms}
|
|
229
|
+
onChange={handleInputChange}
|
|
230
|
+
data-testid="checkbox-terms"
|
|
231
|
+
/>
|
|
232
|
+
<label htmlFor="terms">I agree to the terms and conditions</label>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="button-group">
|
|
237
|
+
<button
|
|
238
|
+
type="submit"
|
|
239
|
+
className="primary"
|
|
240
|
+
disabled={isLoading || !formData.terms}
|
|
241
|
+
data-testid="btn-submit"
|
|
242
|
+
>
|
|
243
|
+
{isLoading ? 'Submitting...' : 'Submit Form'}
|
|
244
|
+
</button>
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
className="secondary"
|
|
248
|
+
onClick={handleReset}
|
|
249
|
+
data-testid="btn-reset"
|
|
250
|
+
>
|
|
251
|
+
Reset
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</form>
|
|
255
|
+
|
|
256
|
+
{submitResult && (
|
|
257
|
+
<div
|
|
258
|
+
className={`result ${submitResult.success ? 'success' : 'error'}`}
|
|
259
|
+
data-testid="submit-result"
|
|
260
|
+
>
|
|
261
|
+
<strong>{submitResult.message}</strong>
|
|
262
|
+
<pre>{JSON.stringify(submitResult.data, null, 2)}</pre>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</section>
|
|
266
|
+
|
|
267
|
+
{/* Interactive Elements Section */}
|
|
268
|
+
<section className="section" data-testid="interactive-section">
|
|
269
|
+
<h2>Interactive Elements</h2>
|
|
270
|
+
|
|
271
|
+
{/* Counter */}
|
|
272
|
+
<div className="form-group">
|
|
273
|
+
<label>Counter</label>
|
|
274
|
+
<div className="counter">
|
|
275
|
+
<button
|
|
276
|
+
className="secondary"
|
|
277
|
+
onClick={() => setCounter((c) => c - 1)}
|
|
278
|
+
data-testid="btn-decrement"
|
|
279
|
+
>
|
|
280
|
+
-
|
|
281
|
+
</button>
|
|
282
|
+
<span className="counter-value" data-testid="counter-value">
|
|
283
|
+
{counter}
|
|
284
|
+
</span>
|
|
285
|
+
<button
|
|
286
|
+
className="secondary"
|
|
287
|
+
onClick={() => setCounter((c) => c + 1)}
|
|
288
|
+
data-testid="btn-increment"
|
|
289
|
+
>
|
|
290
|
+
+
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{/* Toggle */}
|
|
296
|
+
<div className="form-group">
|
|
297
|
+
<label>Toggle Switch</label>
|
|
298
|
+
<div className="toggle-container">
|
|
299
|
+
<label className="toggle">
|
|
300
|
+
<input
|
|
301
|
+
type="checkbox"
|
|
302
|
+
checked={toggleEnabled}
|
|
303
|
+
onChange={(e) => setToggleEnabled(e.target.checked)}
|
|
304
|
+
data-testid="toggle-switch"
|
|
305
|
+
/>
|
|
306
|
+
<span className="toggle-slider"></span>
|
|
307
|
+
</label>
|
|
308
|
+
<span data-testid="toggle-status">
|
|
309
|
+
{toggleEnabled ? 'Enabled' : 'Disabled'}
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Dropdown */}
|
|
315
|
+
<div className="form-group">
|
|
316
|
+
<label>Custom Dropdown</label>
|
|
317
|
+
<div className="dropdown">
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
className="secondary"
|
|
321
|
+
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
322
|
+
data-testid="dropdown-trigger"
|
|
323
|
+
>
|
|
324
|
+
{selectedOption || 'Select an option'} ▼
|
|
325
|
+
</button>
|
|
326
|
+
{dropdownOpen && (
|
|
327
|
+
<div className="dropdown-menu" data-testid="dropdown-menu">
|
|
328
|
+
{['Option A', 'Option B', 'Option C'].map((option) => (
|
|
329
|
+
<button
|
|
330
|
+
key={option}
|
|
331
|
+
onClick={() => {
|
|
332
|
+
setSelectedOption(option);
|
|
333
|
+
setDropdownOpen(false);
|
|
334
|
+
}}
|
|
335
|
+
data-testid={`dropdown-option-${option.toLowerCase().replace(' ', '-')}`}
|
|
336
|
+
>
|
|
337
|
+
{option}
|
|
338
|
+
</button>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
{selectedOption && (
|
|
344
|
+
<p data-testid="dropdown-selected">Selected: {selectedOption}</p>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{/* Modal */}
|
|
349
|
+
<div className="form-group">
|
|
350
|
+
<label>Modal Dialog</label>
|
|
351
|
+
<button
|
|
352
|
+
className="primary"
|
|
353
|
+
onClick={() => setShowModal(true)}
|
|
354
|
+
data-testid="btn-open-modal"
|
|
355
|
+
>
|
|
356
|
+
Open Modal
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{/* Dynamic Content */}
|
|
361
|
+
<div className="form-group">
|
|
362
|
+
<label>Dynamic Content Loading</label>
|
|
363
|
+
<button
|
|
364
|
+
className="primary"
|
|
365
|
+
onClick={loadDynamicContent}
|
|
366
|
+
disabled={isLoading}
|
|
367
|
+
data-testid="btn-load-content"
|
|
368
|
+
>
|
|
369
|
+
{isLoading ? 'Loading...' : 'Load Content'}
|
|
370
|
+
</button>
|
|
371
|
+
{dynamicContent && (
|
|
372
|
+
<div className="result animated" data-testid="dynamic-content">
|
|
373
|
+
<strong>{dynamicContent.title}</strong>
|
|
374
|
+
<ul>
|
|
375
|
+
{dynamicContent.items.map((item, i) => (
|
|
376
|
+
<li key={i} data-testid={`dynamic-item-${i}`}>
|
|
377
|
+
{item}
|
|
378
|
+
</li>
|
|
379
|
+
))}
|
|
380
|
+
</ul>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
</section>
|
|
385
|
+
|
|
386
|
+
{/* Scroll Test Section */}
|
|
387
|
+
<section className="section" data-testid="scroll-section">
|
|
388
|
+
<h2>Scroll Test</h2>
|
|
389
|
+
<div className="scroll-test" data-testid="scroll-container">
|
|
390
|
+
{Array.from({ length: 20 }, (_, i) => (
|
|
391
|
+
<div
|
|
392
|
+
key={i}
|
|
393
|
+
className={`scroll-item ${i === 15 ? 'target' : ''}`}
|
|
394
|
+
data-testid={`scroll-item-${i}`}
|
|
395
|
+
>
|
|
396
|
+
{i === 15 ? (
|
|
397
|
+
<>
|
|
398
|
+
<strong>Target Element</strong>
|
|
399
|
+
<p>This is the element to scroll to</p>
|
|
400
|
+
<button
|
|
401
|
+
className="primary"
|
|
402
|
+
data-testid="scroll-target-button"
|
|
403
|
+
onClick={() => alert('Target clicked!')}
|
|
404
|
+
>
|
|
405
|
+
Click Me
|
|
406
|
+
</button>
|
|
407
|
+
</>
|
|
408
|
+
) : (
|
|
409
|
+
<>Item {i + 1}</>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
</section>
|
|
415
|
+
|
|
416
|
+
{/* Navigation Section */}
|
|
417
|
+
<section className="section" data-testid="navigation-section">
|
|
418
|
+
<h2>Navigation Test</h2>
|
|
419
|
+
<div className="button-group">
|
|
420
|
+
<a href="/page-1" data-testid="link-page-1">
|
|
421
|
+
Go to Page 1
|
|
422
|
+
</a>
|
|
423
|
+
<a href="/page-2" data-testid="link-page-2">
|
|
424
|
+
Go to Page 2
|
|
425
|
+
</a>
|
|
426
|
+
<button
|
|
427
|
+
className="primary"
|
|
428
|
+
onClick={() => (window.location.href = '/success')}
|
|
429
|
+
data-testid="btn-navigate"
|
|
430
|
+
>
|
|
431
|
+
Navigate to Success
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
</section>
|
|
435
|
+
|
|
436
|
+
{/* Modal */}
|
|
437
|
+
{showModal && (
|
|
438
|
+
<div className="modal-overlay" data-testid="modal-overlay">
|
|
439
|
+
<div className="modal" data-testid="modal">
|
|
440
|
+
<h3>Modal Title</h3>
|
|
441
|
+
<p>This is modal content. You can close it or confirm.</p>
|
|
442
|
+
<input
|
|
443
|
+
type="text"
|
|
444
|
+
placeholder="Enter something..."
|
|
445
|
+
data-testid="modal-input"
|
|
446
|
+
/>
|
|
447
|
+
<div className="modal-buttons">
|
|
448
|
+
<button
|
|
449
|
+
className="secondary"
|
|
450
|
+
onClick={() => setShowModal(false)}
|
|
451
|
+
data-testid="modal-cancel"
|
|
452
|
+
>
|
|
453
|
+
Cancel
|
|
454
|
+
</button>
|
|
455
|
+
<button
|
|
456
|
+
className="primary"
|
|
457
|
+
onClick={() => {
|
|
458
|
+
alert('Confirmed!');
|
|
459
|
+
setShowModal(false);
|
|
460
|
+
}}
|
|
461
|
+
data-testid="modal-confirm"
|
|
462
|
+
>
|
|
463
|
+
Confirm
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export default App;
|