assure-testing 1.0.0 โ 1.0.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/README.md +6 -1
- package/dist/commands/open.js +4 -2
- package/dist/commands/otp.js +185 -0
- package/dist/commands/wait-for.js +40 -0
- package/dist/engine/browser.js +179 -9
- package/dist/engine/executor.js +19 -5
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Assure** is a custom testing language (DSL) built from scratch with a unique browser automation engine using Chrome DevTools Protocol (CDP).
|
|
4
4
|
|
|
5
|
+
๐ **[Complete Tutorial โ](TUTORIAL.md)** | ๐ **[Quick Start Guide โ](QUICK_START.md)** | ๐ **[Documentation Site โ](https://upendra-manike.github.io/assure/)** | ๐ฆ [npm](https://www.npmjs.com/package/assure-testing) | ๐ [GitHub](https://github.com/upendra-manike/assure)
|
|
6
|
+
|
|
5
7
|
## โจ Features
|
|
6
8
|
|
|
7
9
|
- ๐ฏ **Custom Syntax** - Human-readable test commands
|
|
@@ -48,7 +50,7 @@ npm install --save-dev assure-testing
|
|
|
48
50
|
### Install from source
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
|
-
git clone https://github.com/
|
|
53
|
+
git clone https://github.com/upendra-manike/assure.git
|
|
52
54
|
cd assure
|
|
53
55
|
npm install
|
|
54
56
|
npm run build
|
|
@@ -230,6 +232,9 @@ session = await createBrowser(false); // visible browser
|
|
|
230
232
|
| `EXPECT URL` | Assert URL | `EXPECT URL CONTAINS "/home"` |
|
|
231
233
|
| `EXPECT TEXT` | Assert text | `EXPECT TEXT "#el" CONTAINS "text"` |
|
|
232
234
|
| `EXPECT VISIBLE` | Assert visibility | `EXPECT VISIBLE "#modal"` |
|
|
235
|
+
| `OTP MANUAL` | Enter OTP manually | `OTP MANUAL "123456" "#otp-input"` |
|
|
236
|
+
| `OTP FROM FILE` | Read OTP from file | `OTP FROM FILE "otp.txt" "#otp-input"` |
|
|
237
|
+
| `OTP FROM CLIPBOARD` | Read OTP from clipboard | `OTP FROM CLIPBOARD "#otp-input"` |
|
|
233
238
|
| `TEST` | Test label | `TEST "My Test"` |
|
|
234
239
|
|
|
235
240
|
## ๐ง Future Enhancements
|
package/dist/commands/open.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OPEN command - Navigates to a URL
|
|
2
|
+
* OPEN command - Navigates to a URL (enhanced for dynamic pages)
|
|
3
3
|
*/
|
|
4
|
-
import { navigate } from '../engine/browser.js';
|
|
4
|
+
import { navigate, waitForNetworkIdle } from '../engine/browser.js';
|
|
5
5
|
export async function executeOpen(session, args) {
|
|
6
6
|
if (args.length === 0) {
|
|
7
7
|
throw new Error('OPEN command requires a URL argument');
|
|
8
8
|
}
|
|
9
9
|
const url = args[0];
|
|
10
10
|
await navigate(session, url);
|
|
11
|
+
// Wait for network to be idle (handles dynamic content loading)
|
|
12
|
+
await waitForNetworkIdle(session);
|
|
11
13
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTP command - Handles One-Time Password validation
|
|
3
|
+
* Supports reading OTP from email, SMS, clipboard, or manual input
|
|
4
|
+
*/
|
|
5
|
+
import { typeText, waitForElementVisible } from '../engine/browser.js';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
/**
|
|
8
|
+
* Extract OTP from text (common patterns)
|
|
9
|
+
*/
|
|
10
|
+
function extractOTP(text) {
|
|
11
|
+
// Common OTP patterns
|
|
12
|
+
const patterns = [
|
|
13
|
+
/\b\d{4}\b/, // 4 digits
|
|
14
|
+
/\b\d{6}\b/, // 6 digits
|
|
15
|
+
/\b\d{8}\b/, // 8 digits
|
|
16
|
+
/code[:\s]+(\d{4,8})/i, // "code: 123456"
|
|
17
|
+
/otp[:\s]+(\d{4,8})/i, // "otp: 123456"
|
|
18
|
+
/verification[:\s]+(\d{4,8})/i, // "verification: 123456"
|
|
19
|
+
/(\d{4,8})/g, // Any 4-8 digit number
|
|
20
|
+
];
|
|
21
|
+
for (const pattern of patterns) {
|
|
22
|
+
const match = text.match(pattern);
|
|
23
|
+
if (match) {
|
|
24
|
+
// Return the first match, prefer longer matches
|
|
25
|
+
const matches = text.match(/\d{4,8}/g);
|
|
26
|
+
if (matches && matches.length > 0) {
|
|
27
|
+
// Return the longest match (most likely to be OTP)
|
|
28
|
+
return matches.sort((a, b) => b.length - a.length)[0];
|
|
29
|
+
}
|
|
30
|
+
return match[1] || match[0];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read OTP from email (simulated - in real scenario, would connect to email service)
|
|
37
|
+
*/
|
|
38
|
+
async function readOTPFromEmail(emailService) {
|
|
39
|
+
// In a real implementation, this would:
|
|
40
|
+
// 1. Connect to email service (Gmail API, IMAP, etc.)
|
|
41
|
+
// 2. Fetch latest email
|
|
42
|
+
// 3. Extract OTP from email body
|
|
43
|
+
// 4. Return OTP
|
|
44
|
+
// For now, return null - user should provide OTP manually or via file
|
|
45
|
+
console.log('โ ๏ธ Email OTP reading requires email service integration');
|
|
46
|
+
console.log(' Please use OTP FROM FILE or OTP MANUAL instead');
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read OTP from SMS (simulated - in real scenario, would connect to SMS service)
|
|
51
|
+
*/
|
|
52
|
+
async function readOTPFromSMS(phoneNumber) {
|
|
53
|
+
// In a real implementation, this would:
|
|
54
|
+
// 1. Connect to SMS service (Twilio, AWS SNS, etc.)
|
|
55
|
+
// 2. Fetch latest SMS
|
|
56
|
+
// 3. Extract OTP from SMS body
|
|
57
|
+
// 4. Return OTP
|
|
58
|
+
console.log('โ ๏ธ SMS OTP reading requires SMS service integration');
|
|
59
|
+
console.log(' Please use OTP FROM FILE or OTP MANUAL instead');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read OTP from clipboard
|
|
64
|
+
*/
|
|
65
|
+
async function readOTPFromClipboard() {
|
|
66
|
+
try {
|
|
67
|
+
// Try to read from clipboard using system command
|
|
68
|
+
const { execSync } = require('child_process');
|
|
69
|
+
const platform = process.platform;
|
|
70
|
+
let clipboardContent = '';
|
|
71
|
+
if (platform === 'darwin') {
|
|
72
|
+
// macOS
|
|
73
|
+
clipboardContent = execSync('pbpaste', { encoding: 'utf-8' });
|
|
74
|
+
}
|
|
75
|
+
else if (platform === 'linux') {
|
|
76
|
+
// Linux (requires xclip or xsel)
|
|
77
|
+
try {
|
|
78
|
+
clipboardContent = execSync('xclip -selection clipboard -o', { encoding: 'utf-8' });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
clipboardContent = execSync('xsel --clipboard --output', { encoding: 'utf-8' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (platform === 'win32') {
|
|
85
|
+
// Windows
|
|
86
|
+
clipboardContent = execSync('powershell -command "Get-Clipboard"', { encoding: 'utf-8' });
|
|
87
|
+
}
|
|
88
|
+
if (clipboardContent) {
|
|
89
|
+
const otp = extractOTP(clipboardContent.trim());
|
|
90
|
+
if (otp) {
|
|
91
|
+
return otp;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Clipboard reading failed
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read OTP from file
|
|
102
|
+
*/
|
|
103
|
+
function readOTPFromFile(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
106
|
+
const otp = extractOTP(content.trim());
|
|
107
|
+
return otp;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`Failed to read OTP from file: ${filePath}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Execute OTP command
|
|
115
|
+
*/
|
|
116
|
+
export async function executeOTP(session, args) {
|
|
117
|
+
if (args.length < 2) {
|
|
118
|
+
throw new Error('OTP command requires: OTP FROM <source> [selector] or OTP MANUAL <code> [selector]');
|
|
119
|
+
}
|
|
120
|
+
const action = args[0].toUpperCase();
|
|
121
|
+
const source = args[1];
|
|
122
|
+
const selector = args.length > 2 ? args[2] : 'input[type="text"]'; // Default OTP input selector
|
|
123
|
+
let otpCode = null;
|
|
124
|
+
// Wait for OTP input field to be ready
|
|
125
|
+
await waitForElementVisible(session, selector);
|
|
126
|
+
if (action === 'FROM') {
|
|
127
|
+
const sourceType = source.toUpperCase();
|
|
128
|
+
switch (sourceType) {
|
|
129
|
+
case 'EMAIL':
|
|
130
|
+
otpCode = await readOTPFromEmail();
|
|
131
|
+
if (!otpCode) {
|
|
132
|
+
throw new Error('Could not read OTP from email. Use OTP FROM FILE or OTP MANUAL instead.');
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'SMS':
|
|
136
|
+
otpCode = await readOTPFromSMS();
|
|
137
|
+
if (!otpCode) {
|
|
138
|
+
throw new Error('Could not read OTP from SMS. Use OTP FROM FILE or OTP MANUAL instead.');
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'CLIPBOARD':
|
|
142
|
+
otpCode = await readOTPFromClipboard();
|
|
143
|
+
if (!otpCode) {
|
|
144
|
+
throw new Error('Could not read OTP from clipboard. Make sure OTP is copied to clipboard.');
|
|
145
|
+
}
|
|
146
|
+
console.log(`โ OTP read from clipboard: ${otpCode}`);
|
|
147
|
+
break;
|
|
148
|
+
case 'FILE':
|
|
149
|
+
if (args.length < 3) {
|
|
150
|
+
throw new Error('OTP FROM FILE requires a file path');
|
|
151
|
+
}
|
|
152
|
+
const filePath = args[2];
|
|
153
|
+
const selectorForFile = args.length > 3 ? args[3] : selector;
|
|
154
|
+
otpCode = readOTPFromFile(filePath);
|
|
155
|
+
if (!otpCode) {
|
|
156
|
+
throw new Error(`Could not extract OTP from file: ${filePath}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(`โ OTP read from file: ${otpCode}`);
|
|
159
|
+
await typeText(session, selectorForFile, otpCode);
|
|
160
|
+
return;
|
|
161
|
+
default:
|
|
162
|
+
throw new Error(`Unknown OTP source: ${source}. Use EMAIL, SMS, CLIPBOARD, or FILE`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (action === 'MANUAL') {
|
|
166
|
+
// OTP MANUAL <code> [selector]
|
|
167
|
+
otpCode = args[1];
|
|
168
|
+
const manualSelector = args.length > 2 ? args[2] : selector;
|
|
169
|
+
if (!otpCode || !/^\d{4,8}$/.test(otpCode)) {
|
|
170
|
+
throw new Error(`Invalid OTP code: ${otpCode}. OTP should be 4-8 digits`);
|
|
171
|
+
}
|
|
172
|
+
console.log(`โ Using manual OTP: ${otpCode}`);
|
|
173
|
+
await typeText(session, manualSelector, otpCode);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
throw new Error(`Unknown OTP action: ${action}. Use FROM or MANUAL`);
|
|
178
|
+
}
|
|
179
|
+
if (!otpCode) {
|
|
180
|
+
throw new Error('Failed to get OTP code');
|
|
181
|
+
}
|
|
182
|
+
// Type OTP into the input field
|
|
183
|
+
await typeText(session, selector, otpCode);
|
|
184
|
+
console.log(`โ OTP entered: ${otpCode}`);
|
|
185
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAIT FOR command - Wait for dynamic elements and conditions
|
|
3
|
+
*/
|
|
4
|
+
import { waitForElementVisible, waitForText, waitForUrl, waitForNetworkIdle } from '../engine/browser.js';
|
|
5
|
+
export async function executeWaitFor(session, args) {
|
|
6
|
+
if (args.length < 2) {
|
|
7
|
+
throw new Error('WAIT FOR command requires a condition and value');
|
|
8
|
+
}
|
|
9
|
+
const condition = args[0].toUpperCase();
|
|
10
|
+
const value = args[1];
|
|
11
|
+
switch (condition) {
|
|
12
|
+
case 'ELEMENT':
|
|
13
|
+
// WAIT FOR ELEMENT selector
|
|
14
|
+
if (args.length < 2) {
|
|
15
|
+
throw new Error('WAIT FOR ELEMENT requires a selector');
|
|
16
|
+
}
|
|
17
|
+
const selector = args[1];
|
|
18
|
+
await waitForElementVisible(session, selector);
|
|
19
|
+
break;
|
|
20
|
+
case 'TEXT':
|
|
21
|
+
// WAIT FOR TEXT selector "text"
|
|
22
|
+
if (args.length < 3) {
|
|
23
|
+
throw new Error('WAIT FOR TEXT requires a selector and text');
|
|
24
|
+
}
|
|
25
|
+
const textSelector = args[1];
|
|
26
|
+
const textToWait = args.slice(2).join(' ');
|
|
27
|
+
await waitForText(session, textSelector, textToWait);
|
|
28
|
+
break;
|
|
29
|
+
case 'URL':
|
|
30
|
+
// WAIT FOR URL "url"
|
|
31
|
+
await waitForUrl(session, value);
|
|
32
|
+
break;
|
|
33
|
+
case 'NETWORK':
|
|
34
|
+
// WAIT FOR NETWORK IDLE
|
|
35
|
+
await waitForNetworkIdle(session);
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unknown WAIT FOR condition: ${condition}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/engine/browser.js
CHANGED
|
@@ -143,16 +143,20 @@ export async function getUrl(session) {
|
|
|
143
143
|
return result.result.value;
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
* Wait for element to be available
|
|
146
|
+
* Wait for element to be available (enhanced for dynamic elements)
|
|
147
147
|
*/
|
|
148
|
-
export async function waitForSelector(session, selector, timeout =
|
|
148
|
+
export async function waitForSelector(session, selector, timeout = 10000) {
|
|
149
149
|
const startTime = Date.now();
|
|
150
150
|
const checkInterval = 100;
|
|
151
151
|
while (Date.now() - startTime < timeout) {
|
|
152
152
|
try {
|
|
153
153
|
const nodeId = await querySelector(session, selector);
|
|
154
154
|
if (nodeId !== null) {
|
|
155
|
-
|
|
155
|
+
// Also check if element is visible and in viewport
|
|
156
|
+
const isVisible = await checkElementVisible(session, nodeId);
|
|
157
|
+
if (isVisible) {
|
|
158
|
+
return nodeId;
|
|
159
|
+
}
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
catch (error) {
|
|
@@ -160,7 +164,170 @@ export async function waitForSelector(session, selector, timeout = 5000) {
|
|
|
160
164
|
}
|
|
161
165
|
await setTimeout(checkInterval);
|
|
162
166
|
}
|
|
163
|
-
throw new Error(`Element "${selector}" not found within ${timeout}ms`);
|
|
167
|
+
throw new Error(`Element "${selector}" not found or not visible within ${timeout}ms`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wait for element to be visible and clickable
|
|
171
|
+
*/
|
|
172
|
+
export async function waitForElementVisible(session, selector, timeout = 10000) {
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
const checkInterval = 100;
|
|
175
|
+
while (Date.now() - startTime < timeout) {
|
|
176
|
+
try {
|
|
177
|
+
const nodeId = await querySelector(session, selector);
|
|
178
|
+
if (nodeId !== null) {
|
|
179
|
+
const visible = await checkElementVisible(session, nodeId);
|
|
180
|
+
const clickable = await checkElementClickable(session, nodeId);
|
|
181
|
+
if (visible && clickable) {
|
|
182
|
+
return nodeId;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// Continue waiting
|
|
188
|
+
}
|
|
189
|
+
await setTimeout(checkInterval);
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`Element "${selector}" not visible or clickable within ${timeout}ms`);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if element is visible in viewport
|
|
195
|
+
*/
|
|
196
|
+
async function checkElementVisible(session, nodeId) {
|
|
197
|
+
try {
|
|
198
|
+
const result = await session.DOM.resolveNode({ nodeId });
|
|
199
|
+
if (!result.object.objectId)
|
|
200
|
+
return false;
|
|
201
|
+
const visibilityResult = await session.Runtime.callFunctionOn({
|
|
202
|
+
objectId: result.object.objectId,
|
|
203
|
+
functionDeclaration: `
|
|
204
|
+
function() {
|
|
205
|
+
const rect = this.getBoundingClientRect();
|
|
206
|
+
const style = window.getComputedStyle(this);
|
|
207
|
+
return (
|
|
208
|
+
style.display !== 'none' &&
|
|
209
|
+
style.visibility !== 'hidden' &&
|
|
210
|
+
style.opacity !== '0' &&
|
|
211
|
+
rect.width > 0 &&
|
|
212
|
+
rect.height > 0 &&
|
|
213
|
+
rect.top >= 0 &&
|
|
214
|
+
rect.left >= 0 &&
|
|
215
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
216
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
`,
|
|
220
|
+
returnByValue: true
|
|
221
|
+
});
|
|
222
|
+
return visibilityResult.result.value === true;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if element is clickable (not disabled, not covered)
|
|
230
|
+
*/
|
|
231
|
+
async function checkElementClickable(session, nodeId) {
|
|
232
|
+
try {
|
|
233
|
+
const result = await session.DOM.resolveNode({ nodeId });
|
|
234
|
+
if (!result.object.objectId)
|
|
235
|
+
return false;
|
|
236
|
+
const clickableResult = await session.Runtime.callFunctionOn({
|
|
237
|
+
objectId: result.object.objectId,
|
|
238
|
+
functionDeclaration: `
|
|
239
|
+
function() {
|
|
240
|
+
if (this.disabled) return false;
|
|
241
|
+
if (this.getAttribute('aria-disabled') === 'true') return false;
|
|
242
|
+
|
|
243
|
+
// Check if element is covered by another element
|
|
244
|
+
const rect = this.getBoundingClientRect();
|
|
245
|
+
const centerX = rect.left + rect.width / 2;
|
|
246
|
+
const centerY = rect.top + rect.height / 2;
|
|
247
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
248
|
+
|
|
249
|
+
return topElement === this || this.contains(topElement);
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
returnByValue: true
|
|
253
|
+
});
|
|
254
|
+
return clickableResult.result.value === true;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Wait for text to appear in element
|
|
262
|
+
*/
|
|
263
|
+
export async function waitForText(session, selector, text, timeout = 10000) {
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
const checkInterval = 200;
|
|
266
|
+
while (Date.now() - startTime < timeout) {
|
|
267
|
+
try {
|
|
268
|
+
const elementText = await getTextContent(session, selector);
|
|
269
|
+
if (elementText.includes(text)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// Continue waiting
|
|
275
|
+
}
|
|
276
|
+
await setTimeout(checkInterval);
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Text "${text}" not found in element "${selector}" within ${timeout}ms`);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Wait for URL to change or contain specific text
|
|
282
|
+
*/
|
|
283
|
+
export async function waitForUrl(session, expectedUrl, timeout = 10000) {
|
|
284
|
+
const startTime = Date.now();
|
|
285
|
+
const checkInterval = 200;
|
|
286
|
+
while (Date.now() - startTime < timeout) {
|
|
287
|
+
try {
|
|
288
|
+
const currentUrl = await getUrl(session);
|
|
289
|
+
if (currentUrl.includes(expectedUrl)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
// Continue waiting
|
|
295
|
+
}
|
|
296
|
+
await setTimeout(checkInterval);
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`URL did not contain "${expectedUrl}" within ${timeout}ms`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Wait for network to be idle (no pending requests)
|
|
302
|
+
*/
|
|
303
|
+
export async function waitForNetworkIdle(session, timeout = 5000) {
|
|
304
|
+
// Wait a bit for any pending requests
|
|
305
|
+
await setTimeout(500);
|
|
306
|
+
// Check if there are active network requests
|
|
307
|
+
const startTime = Date.now();
|
|
308
|
+
const checkInterval = 100;
|
|
309
|
+
while (Date.now() - startTime < timeout) {
|
|
310
|
+
try {
|
|
311
|
+
const result = await session.Runtime.evaluate({
|
|
312
|
+
expression: `
|
|
313
|
+
(function() {
|
|
314
|
+
if (typeof window.fetch === 'undefined') return true;
|
|
315
|
+
// Simple check - in real implementation, track fetch/XHR requests
|
|
316
|
+
return document.readyState === 'complete';
|
|
317
|
+
})()
|
|
318
|
+
`,
|
|
319
|
+
returnByValue: true
|
|
320
|
+
});
|
|
321
|
+
if (result.result.value === true) {
|
|
322
|
+
await setTimeout(500); // Wait a bit more to ensure stability
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// Continue waiting
|
|
328
|
+
}
|
|
329
|
+
await setTimeout(checkInterval);
|
|
330
|
+
}
|
|
164
331
|
}
|
|
165
332
|
/**
|
|
166
333
|
* Query selector and return nodeId
|
|
@@ -174,10 +341,11 @@ export async function querySelector(session, selector) {
|
|
|
174
341
|
return nodeId;
|
|
175
342
|
}
|
|
176
343
|
/**
|
|
177
|
-
* Click an element
|
|
344
|
+
* Click an element (enhanced for dynamic elements)
|
|
178
345
|
*/
|
|
179
346
|
export async function clickElement(session, selector) {
|
|
180
|
-
|
|
347
|
+
// Wait for element to be visible and clickable (handles dynamic elements)
|
|
348
|
+
const nodeId = await waitForElementVisible(session, selector);
|
|
181
349
|
// Get bounding box
|
|
182
350
|
const boxModel = await session.DOM.getBoxModel({ nodeId });
|
|
183
351
|
if (!boxModel.model) {
|
|
@@ -205,10 +373,11 @@ export async function clickElement(session, selector) {
|
|
|
205
373
|
await setTimeout(100);
|
|
206
374
|
}
|
|
207
375
|
/**
|
|
208
|
-
* Type text into an input field
|
|
376
|
+
* Type text into an input field (enhanced for dynamic elements)
|
|
209
377
|
*/
|
|
210
378
|
export async function typeText(session, selector, text) {
|
|
211
|
-
|
|
379
|
+
// Wait for element to be visible and ready (handles dynamic elements)
|
|
380
|
+
const nodeId = await waitForElementVisible(session, selector);
|
|
212
381
|
// Focus the element
|
|
213
382
|
await session.DOM.focus({ nodeId });
|
|
214
383
|
// Clear existing value using nodeId
|
|
@@ -229,9 +398,10 @@ export async function typeText(session, selector, text) {
|
|
|
229
398
|
}
|
|
230
399
|
}
|
|
231
400
|
/**
|
|
232
|
-
* Get text content of an element
|
|
401
|
+
* Get text content of an element (enhanced for dynamic elements)
|
|
233
402
|
*/
|
|
234
403
|
export async function getTextContent(session, selector) {
|
|
404
|
+
// Wait for element to be available (handles dynamic elements)
|
|
235
405
|
const nodeId = await waitForSelector(session, selector);
|
|
236
406
|
const result = await session.DOM.resolveNode({ nodeId });
|
|
237
407
|
if (result.object.objectId) {
|
package/dist/engine/executor.js
CHANGED
|
@@ -5,6 +5,8 @@ import { executeOpen } from '../commands/open.js';
|
|
|
5
5
|
import { executeClick } from '../commands/click.js';
|
|
6
6
|
import { executeType } from '../commands/type.js';
|
|
7
7
|
import { executeExpect } from '../commands/expect.js';
|
|
8
|
+
import { executeWaitFor } from '../commands/wait-for.js';
|
|
9
|
+
import { executeOTP } from '../commands/otp.js';
|
|
8
10
|
import { wait } from './browser.js';
|
|
9
11
|
export async function execute(commands, session) {
|
|
10
12
|
for (const cmd of commands) {
|
|
@@ -24,17 +26,29 @@ export async function execute(commands, session) {
|
|
|
24
26
|
console.log(`โ Line ${lineNumber}: TYPE "${args[0]}" "${args.slice(1).join(' ')}"`);
|
|
25
27
|
break;
|
|
26
28
|
case 'WAIT':
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
29
|
+
// Check if it's WAIT FOR (dynamic element waiting)
|
|
30
|
+
if (args.length > 0 && args[0].toUpperCase() === 'FOR') {
|
|
31
|
+
await executeWaitFor(session, args.slice(1));
|
|
32
|
+
console.log(`โ Line ${lineNumber}: WAIT FOR ${args.slice(1).join(' ')}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Regular WAIT command
|
|
36
|
+
const seconds = Number(args[0]);
|
|
37
|
+
if (isNaN(seconds)) {
|
|
38
|
+
throw new Error(`WAIT command requires a valid number, got: ${args[0]}`);
|
|
39
|
+
}
|
|
40
|
+
await wait(seconds);
|
|
41
|
+
console.log(`โ Line ${lineNumber}: WAIT ${seconds}`);
|
|
30
42
|
}
|
|
31
|
-
await wait(seconds);
|
|
32
|
-
console.log(`โ Line ${lineNumber}: WAIT ${seconds}`);
|
|
33
43
|
break;
|
|
34
44
|
case 'EXPECT':
|
|
35
45
|
await executeExpect(session, args);
|
|
36
46
|
console.log(`โ Line ${lineNumber}: EXPECT ${args.join(' ')}`);
|
|
37
47
|
break;
|
|
48
|
+
case 'OTP':
|
|
49
|
+
await executeOTP(session, args);
|
|
50
|
+
console.log(`โ Line ${lineNumber}: OTP ${args.join(' ')}`);
|
|
51
|
+
break;
|
|
38
52
|
case 'TEST':
|
|
39
53
|
// TEST is just a label, skip execution
|
|
40
54
|
console.log(`\n๐งช ${args.join(' ')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assure-testing",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Assure - A custom testing language (DSL) for browser automation. Test files use .assure extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/runner.js",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/
|
|
39
|
+
"url": "https://github.com/upendra-manike/assure.git"
|
|
40
40
|
},
|
|
41
41
|
"bugs": {
|
|
42
|
-
"url": "https://github.com/
|
|
42
|
+
"url": "https://github.com/upendra-manike/assure/issues"
|
|
43
43
|
},
|
|
44
|
-
"homepage": "https://github.com/
|
|
44
|
+
"homepage": "https://github.com/upendra-manike/assure#readme",
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=18.0.0"
|
|
47
47
|
},
|
|
@@ -54,4 +54,3 @@
|
|
|
54
54
|
"typescript": "^5.3.0"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|