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 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/yourusername/assure.git
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
@@ -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
+ }
@@ -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 = 5000) {
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
- return nodeId;
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
- const nodeId = await waitForSelector(session, selector);
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
- const nodeId = await waitForSelector(session, selector);
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) {
@@ -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
- const seconds = Number(args[0]);
28
- if (isNaN(seconds)) {
29
- throw new Error(`WAIT command requires a valid number, got: ${args[0]}`);
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.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/yourusername/assure.git"
39
+ "url": "https://github.com/upendra-manike/assure.git"
40
40
  },
41
41
  "bugs": {
42
- "url": "https://github.com/yourusername/assure/issues"
42
+ "url": "https://github.com/upendra-manike/assure/issues"
43
43
  },
44
- "homepage": "https://github.com/yourusername/assure#readme",
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
-