assure-testing 1.0.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/LICENSE +22 -0
- package/README.md +257 -0
- package/dist/commands/click.js +11 -0
- package/dist/commands/expect.js +79 -0
- package/dist/commands/open.js +11 -0
- package/dist/commands/type.js +12 -0
- package/dist/engine/browser.js +279 -0
- package/dist/engine/executor.js +52 -0
- package/dist/language/parser.js +26 -0
- package/dist/language/tokenizer.js +42 -0
- package/dist/runner.js +74 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Assure Testing Language
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# ๐งช Assure - Custom Testing Language
|
|
2
|
+
|
|
3
|
+
**Assure** is a custom testing language (DSL) built from scratch with a unique browser automation engine using Chrome DevTools Protocol (CDP).
|
|
4
|
+
|
|
5
|
+
## โจ Features
|
|
6
|
+
|
|
7
|
+
- ๐ฏ **Custom Syntax** - Human-readable test commands
|
|
8
|
+
- ๐ง **Built from Scratch** - No Playwright, no Selenium - pure CDP implementation
|
|
9
|
+
- โก **Lightweight** - Minimal dependencies
|
|
10
|
+
- ๐ **Fast** - Direct Chrome DevTools Protocol communication
|
|
11
|
+
- ๐ **Simple** - Easy to learn and write tests
|
|
12
|
+
|
|
13
|
+
## ๐๏ธ Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
assure/
|
|
17
|
+
โ
|
|
18
|
+
โโโ language/
|
|
19
|
+
โ โโโ parser.ts # Parses Assure syntax
|
|
20
|
+
โ โโโ tokenizer.ts # Tokenizes lines
|
|
21
|
+
โ
|
|
22
|
+
โโโ engine/
|
|
23
|
+
โ โโโ browser.ts # Custom CDP browser engine
|
|
24
|
+
โ โโโ executor.ts # Command executor
|
|
25
|
+
โ
|
|
26
|
+
โโโ commands/
|
|
27
|
+
โ โโโ open.ts # OPEN command
|
|
28
|
+
โ โโโ click.ts # CLICK command
|
|
29
|
+
โ โโโ type.ts # TYPE command
|
|
30
|
+
โ โโโ expect.ts # EXPECT command
|
|
31
|
+
โ
|
|
32
|
+
โโโ runner.ts # Main test runner
|
|
33
|
+
โโโ *.assure # Test files
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ๐ฆ Installation
|
|
37
|
+
|
|
38
|
+
### Install from npm (Recommended)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install globally
|
|
42
|
+
npm install -g assure-testing
|
|
43
|
+
|
|
44
|
+
# Or install locally in your project
|
|
45
|
+
npm install --save-dev assure-testing
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Install from source
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/yourusername/assure.git
|
|
52
|
+
cd assure
|
|
53
|
+
npm install
|
|
54
|
+
npm run build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Requirements:**
|
|
58
|
+
- Node.js 18+
|
|
59
|
+
- Chrome or Chromium browser installed
|
|
60
|
+
|
|
61
|
+
**File Extension:**
|
|
62
|
+
- All test files must use the `.assure` extension (e.g., `test.assure`, `login.assure`)
|
|
63
|
+
|
|
64
|
+
## ๐ Quick Start
|
|
65
|
+
|
|
66
|
+
1. **Create a test file with `.assure` extension** (`example.assure`):
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
TEST "My First Test"
|
|
70
|
+
|
|
71
|
+
OPEN "https://example.com"
|
|
72
|
+
WAIT 2
|
|
73
|
+
EXPECT TITLE CONTAINS "Example"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
2. **Install dependencies**:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Run the test**:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# If installed globally
|
|
86
|
+
assure example.assure
|
|
87
|
+
|
|
88
|
+
# If installed locally
|
|
89
|
+
npx assure example.assure
|
|
90
|
+
|
|
91
|
+
# Or using npm script
|
|
92
|
+
npm test example.assure
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## ๐ Language Syntax
|
|
96
|
+
|
|
97
|
+
### Basic Commands
|
|
98
|
+
|
|
99
|
+
#### OPEN
|
|
100
|
+
Navigate to a URL:
|
|
101
|
+
```
|
|
102
|
+
OPEN "https://example.com"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### CLICK
|
|
106
|
+
Click an element:
|
|
107
|
+
```
|
|
108
|
+
CLICK "#button"
|
|
109
|
+
CLICK ".submit-btn"
|
|
110
|
+
CLICK "button[type='submit']"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### TYPE
|
|
114
|
+
Type text into an input:
|
|
115
|
+
```
|
|
116
|
+
TYPE "#username" "admin"
|
|
117
|
+
TYPE "#password" "secret123"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### WAIT
|
|
121
|
+
Wait for specified seconds:
|
|
122
|
+
```
|
|
123
|
+
WAIT 2
|
|
124
|
+
WAIT 5
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### EXPECT
|
|
128
|
+
Assert conditions:
|
|
129
|
+
|
|
130
|
+
**Title:**
|
|
131
|
+
```
|
|
132
|
+
EXPECT TITLE CONTAINS "Dashboard"
|
|
133
|
+
EXPECT TITLE EQUALS "My App"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**URL:**
|
|
137
|
+
```
|
|
138
|
+
EXPECT URL CONTAINS "/dashboard"
|
|
139
|
+
EXPECT URL EQUALS "https://example.com/home"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Text:**
|
|
143
|
+
```
|
|
144
|
+
EXPECT TEXT "#welcome" CONTAINS "Welcome"
|
|
145
|
+
EXPECT TEXT ".message" EQUALS "Success"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Visibility:**
|
|
149
|
+
```
|
|
150
|
+
EXPECT VISIBLE "#modal"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Comments
|
|
154
|
+
|
|
155
|
+
Lines starting with `#` are comments:
|
|
156
|
+
```
|
|
157
|
+
# This is a comment
|
|
158
|
+
OPEN "https://example.com"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Test Labels
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
TEST "User Login Flow"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## ๐ง Custom Browser Engine
|
|
168
|
+
|
|
169
|
+
Assure uses a **custom-built browser engine** that communicates directly with Chrome via Chrome DevTools Protocol (CDP). This gives you:
|
|
170
|
+
|
|
171
|
+
- **Full Control** - Direct access to browser internals
|
|
172
|
+
- **No Heavy Dependencies** - Just `chrome-remote-interface` for CDP
|
|
173
|
+
- **Unique Implementation** - Built specifically for Assure
|
|
174
|
+
|
|
175
|
+
### How It Works
|
|
176
|
+
|
|
177
|
+
1. Launches Chrome with remote debugging enabled
|
|
178
|
+
2. Connects via WebSocket to Chrome DevTools Protocol
|
|
179
|
+
3. Executes commands using CDP methods
|
|
180
|
+
4. Handles element selection, clicking, typing, etc.
|
|
181
|
+
|
|
182
|
+
## ๐ Example Test File
|
|
183
|
+
|
|
184
|
+
Save your tests with the `.assure` extension (e.g., `login.assure`, `checkout.assure`):
|
|
185
|
+
|
|
186
|
+
```assure
|
|
187
|
+
TEST "User Login"
|
|
188
|
+
|
|
189
|
+
OPEN "https://example.com/login"
|
|
190
|
+
|
|
191
|
+
TYPE "#username" "admin"
|
|
192
|
+
TYPE "#password" "password123"
|
|
193
|
+
CLICK "#login-button"
|
|
194
|
+
|
|
195
|
+
WAIT 2
|
|
196
|
+
|
|
197
|
+
EXPECT URL CONTAINS "/dashboard"
|
|
198
|
+
EXPECT TEXT "#welcome" CONTAINS "Welcome"
|
|
199
|
+
EXPECT VISIBLE "#user-menu"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## ๐ ๏ธ Configuration
|
|
203
|
+
|
|
204
|
+
### Chrome Path
|
|
205
|
+
|
|
206
|
+
If Chrome is not in the default location, set the `CHROME_PATH` environment variable:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
export CHROME_PATH="/path/to/chrome"
|
|
210
|
+
node runner.js test.assure
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Headless Mode
|
|
214
|
+
|
|
215
|
+
Currently runs in headless mode by default. To run with visible browser, modify `runner.ts`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
session = await createBrowser(false); // visible browser
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## ๐ฏ Supported Commands
|
|
222
|
+
|
|
223
|
+
| Command | Description | Example |
|
|
224
|
+
|---------|-------------|---------|
|
|
225
|
+
| `OPEN` | Navigate to URL | `OPEN "https://example.com"` |
|
|
226
|
+
| `CLICK` | Click element | `CLICK "#button"` |
|
|
227
|
+
| `TYPE` | Type text | `TYPE "#input" "text"` |
|
|
228
|
+
| `WAIT` | Wait seconds | `WAIT 2` |
|
|
229
|
+
| `EXPECT TITLE` | Assert title | `EXPECT TITLE CONTAINS "Page"` |
|
|
230
|
+
| `EXPECT URL` | Assert URL | `EXPECT URL CONTAINS "/home"` |
|
|
231
|
+
| `EXPECT TEXT` | Assert text | `EXPECT TEXT "#el" CONTAINS "text"` |
|
|
232
|
+
| `EXPECT VISIBLE` | Assert visibility | `EXPECT VISIBLE "#modal"` |
|
|
233
|
+
| `TEST` | Test label | `TEST "My Test"` |
|
|
234
|
+
|
|
235
|
+
## ๐ง Future Enhancements
|
|
236
|
+
|
|
237
|
+
- [ ] Variables and functions
|
|
238
|
+
- [ ] IF/ELSE conditionals
|
|
239
|
+
- [ ] RETRY mechanisms
|
|
240
|
+
- [ ] Parallel test execution
|
|
241
|
+
- [ ] HTML/JSON reports
|
|
242
|
+
- [ ] Screenshot support
|
|
243
|
+
- [ ] VS Code syntax highlighting
|
|
244
|
+
- [ ] CI/CD integration
|
|
245
|
+
|
|
246
|
+
## ๐ License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
249
|
+
|
|
250
|
+
## ๐ค Contributing
|
|
251
|
+
|
|
252
|
+
This is a custom testing language built from scratch. Feel free to extend it!
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
**Built with โค๏ธ using Chrome DevTools Protocol**
|
|
257
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLICK command - Clicks an element
|
|
3
|
+
*/
|
|
4
|
+
import { clickElement } from '../engine/browser.js';
|
|
5
|
+
export async function executeClick(session, args) {
|
|
6
|
+
if (args.length === 0) {
|
|
7
|
+
throw new Error('CLICK command requires a selector argument');
|
|
8
|
+
}
|
|
9
|
+
const selector = args[0];
|
|
10
|
+
await clickElement(session, selector);
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXPECT command - Asserts conditions
|
|
3
|
+
* Built from scratch using CDP
|
|
4
|
+
*/
|
|
5
|
+
import { getTitle, getUrl, getTextContent, isVisible } from '../engine/browser.js';
|
|
6
|
+
export async function executeExpect(session, args) {
|
|
7
|
+
if (args.length < 2) {
|
|
8
|
+
throw new Error('EXPECT command requires at least 2 arguments');
|
|
9
|
+
}
|
|
10
|
+
const [target, condition, ...valueParts] = args;
|
|
11
|
+
const value = valueParts.join(' ');
|
|
12
|
+
switch (target.toUpperCase()) {
|
|
13
|
+
case 'TITLE':
|
|
14
|
+
const title = await getTitle(session);
|
|
15
|
+
if (condition.toUpperCase() === 'CONTAINS') {
|
|
16
|
+
if (!title.includes(value)) {
|
|
17
|
+
throw new Error(`Expected title to contain "${value}", but got "${title}"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (condition.toUpperCase() === 'EQUALS') {
|
|
21
|
+
if (title !== value) {
|
|
22
|
+
throw new Error(`Expected title to equal "${value}", but got "${title}"`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(`Unknown condition for TITLE: ${condition}`);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
case 'URL':
|
|
30
|
+
const url = await getUrl(session);
|
|
31
|
+
if (condition.toUpperCase() === 'CONTAINS') {
|
|
32
|
+
if (!url.includes(value)) {
|
|
33
|
+
throw new Error(`Expected URL to contain "${value}", but got "${url}"`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (condition.toUpperCase() === 'EQUALS') {
|
|
37
|
+
if (url !== value) {
|
|
38
|
+
throw new Error(`Expected URL to equal "${value}", but got "${url}"`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw new Error(`Unknown condition for URL: ${condition}`);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case 'TEXT':
|
|
46
|
+
if (args.length < 3) {
|
|
47
|
+
throw new Error('EXPECT TEXT requires a selector, condition, and value');
|
|
48
|
+
}
|
|
49
|
+
// For TEXT, the format is: EXPECT TEXT selector condition value
|
|
50
|
+
// But args already has target removed, so: [selector, condition, ...value]
|
|
51
|
+
const selector = args[0];
|
|
52
|
+
const textCondition = args[1];
|
|
53
|
+
const textValue = args.slice(2).join(' ');
|
|
54
|
+
const elementText = await getTextContent(session, selector);
|
|
55
|
+
if (textCondition.toUpperCase() === 'CONTAINS') {
|
|
56
|
+
if (!elementText.includes(textValue)) {
|
|
57
|
+
throw new Error(`Expected text to contain "${textValue}", but got "${elementText}"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (textCondition.toUpperCase() === 'EQUALS') {
|
|
61
|
+
if (elementText.trim() !== textValue.trim()) {
|
|
62
|
+
throw new Error(`Expected text to equal "${textValue}", but got "${elementText}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new Error(`Unknown condition for TEXT: ${textCondition}`);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case 'VISIBLE':
|
|
70
|
+
const visibleSelector = args[0];
|
|
71
|
+
const visible = await isVisible(session, visibleSelector);
|
|
72
|
+
if (!visible) {
|
|
73
|
+
throw new Error(`Expected element "${visibleSelector}" to be visible`);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unknown EXPECT target: ${target}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPEN command - Navigates to a URL
|
|
3
|
+
*/
|
|
4
|
+
import { navigate } from '../engine/browser.js';
|
|
5
|
+
export async function executeOpen(session, args) {
|
|
6
|
+
if (args.length === 0) {
|
|
7
|
+
throw new Error('OPEN command requires a URL argument');
|
|
8
|
+
}
|
|
9
|
+
const url = args[0];
|
|
10
|
+
await navigate(session, url);
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TYPE command - Types text into an input field
|
|
3
|
+
*/
|
|
4
|
+
import { typeText } from '../engine/browser.js';
|
|
5
|
+
export async function executeType(session, args) {
|
|
6
|
+
if (args.length < 2) {
|
|
7
|
+
throw new Error('TYPE command requires a selector and text argument');
|
|
8
|
+
}
|
|
9
|
+
const selector = args[0];
|
|
10
|
+
const text = args.slice(1).join(' '); // Join in case text has spaces
|
|
11
|
+
await typeText(session, selector, text);
|
|
12
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Engine - Custom CDP-based browser automation
|
|
3
|
+
* Built from scratch using Chrome DevTools Protocol
|
|
4
|
+
*/
|
|
5
|
+
import CDP from 'chrome-remote-interface';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { setTimeout } from 'timers/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
const DEFAULT_CHROME_ARGS = [
|
|
10
|
+
'--headless',
|
|
11
|
+
'--disable-gpu',
|
|
12
|
+
'--no-sandbox',
|
|
13
|
+
'--disable-setuid-sandbox',
|
|
14
|
+
'--disable-dev-shm-usage',
|
|
15
|
+
'--remote-debugging-port=9222'
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Find Chrome/Chromium executable path
|
|
19
|
+
*/
|
|
20
|
+
function findChromeExecutable() {
|
|
21
|
+
const platform = process.platform;
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
// macOS
|
|
24
|
+
const paths = [
|
|
25
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
26
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
27
|
+
'/usr/bin/google-chrome',
|
|
28
|
+
'/usr/bin/chromium'
|
|
29
|
+
];
|
|
30
|
+
for (const path of paths) {
|
|
31
|
+
if (existsSync(path))
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (platform === 'linux') {
|
|
36
|
+
// Try common Linux Chrome/Chromium paths
|
|
37
|
+
const linuxPaths = ['google-chrome', 'chromium', 'chromium-browser'];
|
|
38
|
+
for (const chromePath of linuxPaths) {
|
|
39
|
+
// On Linux, we'll try to execute it to see if it exists
|
|
40
|
+
// For now, return the first one and let spawn handle errors
|
|
41
|
+
return chromePath;
|
|
42
|
+
}
|
|
43
|
+
return 'google-chrome'; // fallback
|
|
44
|
+
}
|
|
45
|
+
else if (platform === 'win32') {
|
|
46
|
+
const paths = [
|
|
47
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
48
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
|
49
|
+
];
|
|
50
|
+
for (const path of paths) {
|
|
51
|
+
if (existsSync(path))
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error('Chrome/Chromium not found. Please install Chrome or set CHROME_PATH environment variable.');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Launch Chrome and connect via CDP
|
|
59
|
+
*/
|
|
60
|
+
export async function createBrowser(headless = true) {
|
|
61
|
+
const chromePath = process.env.CHROME_PATH || findChromeExecutable();
|
|
62
|
+
const args = headless ? DEFAULT_CHROME_ARGS : DEFAULT_CHROME_ARGS.filter(arg => arg !== '--headless');
|
|
63
|
+
// Launch Chrome
|
|
64
|
+
const chromeProcess = spawn(chromePath, args, {
|
|
65
|
+
stdio: 'ignore',
|
|
66
|
+
detached: false
|
|
67
|
+
});
|
|
68
|
+
// Wait for Chrome to start
|
|
69
|
+
await setTimeout(2000);
|
|
70
|
+
// Connect to Chrome via CDP
|
|
71
|
+
let client;
|
|
72
|
+
let retries = 10;
|
|
73
|
+
while (retries > 0) {
|
|
74
|
+
try {
|
|
75
|
+
client = await CDP({ port: 9222 });
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
retries--;
|
|
80
|
+
if (retries === 0) {
|
|
81
|
+
chromeProcess.kill();
|
|
82
|
+
throw new Error('Failed to connect to Chrome DevTools Protocol');
|
|
83
|
+
}
|
|
84
|
+
await setTimeout(500);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!client) {
|
|
88
|
+
chromeProcess.kill();
|
|
89
|
+
throw new Error('Failed to connect to Chrome DevTools Protocol');
|
|
90
|
+
}
|
|
91
|
+
// Enable required domains
|
|
92
|
+
const { Page, Runtime, DOM, Input, Network } = client;
|
|
93
|
+
await Page.enable();
|
|
94
|
+
await Runtime.enable();
|
|
95
|
+
await DOM.enable();
|
|
96
|
+
await Network.enable();
|
|
97
|
+
return {
|
|
98
|
+
chromeProcess,
|
|
99
|
+
client,
|
|
100
|
+
Page,
|
|
101
|
+
Runtime,
|
|
102
|
+
DOM,
|
|
103
|
+
Input,
|
|
104
|
+
Network
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Close browser and cleanup
|
|
109
|
+
*/
|
|
110
|
+
export async function closeBrowser(session) {
|
|
111
|
+
try {
|
|
112
|
+
await session.client.close();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// Ignore close errors
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
session.chromeProcess.kill();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// Ignore kill errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Navigate to URL
|
|
126
|
+
*/
|
|
127
|
+
export async function navigate(session, url) {
|
|
128
|
+
await session.Page.navigate({ url });
|
|
129
|
+
await session.Page.loadEventFired();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get page title
|
|
133
|
+
*/
|
|
134
|
+
export async function getTitle(session) {
|
|
135
|
+
const result = await session.Runtime.evaluate({ expression: 'document.title' });
|
|
136
|
+
return result.result.value;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get current URL
|
|
140
|
+
*/
|
|
141
|
+
export async function getUrl(session) {
|
|
142
|
+
const result = await session.Runtime.evaluate({ expression: 'window.location.href' });
|
|
143
|
+
return result.result.value;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Wait for element to be available
|
|
147
|
+
*/
|
|
148
|
+
export async function waitForSelector(session, selector, timeout = 5000) {
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
const checkInterval = 100;
|
|
151
|
+
while (Date.now() - startTime < timeout) {
|
|
152
|
+
try {
|
|
153
|
+
const nodeId = await querySelector(session, selector);
|
|
154
|
+
if (nodeId !== null) {
|
|
155
|
+
return nodeId;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Element not found, continue waiting
|
|
160
|
+
}
|
|
161
|
+
await setTimeout(checkInterval);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Element "${selector}" not found within ${timeout}ms`);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Query selector and return nodeId
|
|
167
|
+
*/
|
|
168
|
+
export async function querySelector(session, selector) {
|
|
169
|
+
const document = await session.DOM.getDocument();
|
|
170
|
+
const { nodeId } = await session.DOM.querySelector({
|
|
171
|
+
nodeId: document.root.nodeId,
|
|
172
|
+
selector: selector
|
|
173
|
+
});
|
|
174
|
+
return nodeId;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Click an element
|
|
178
|
+
*/
|
|
179
|
+
export async function clickElement(session, selector) {
|
|
180
|
+
const nodeId = await waitForSelector(session, selector);
|
|
181
|
+
// Get bounding box
|
|
182
|
+
const boxModel = await session.DOM.getBoxModel({ nodeId });
|
|
183
|
+
if (!boxModel.model) {
|
|
184
|
+
throw new Error(`Could not get bounding box for selector: ${selector}`);
|
|
185
|
+
}
|
|
186
|
+
const content = boxModel.model.content;
|
|
187
|
+
const x = (content[0] + content[2]) / 2; // Center X
|
|
188
|
+
const y = (content[1] + content[5]) / 2; // Center Y
|
|
189
|
+
// Click at coordinates
|
|
190
|
+
await session.Input.dispatchMouseEvent({
|
|
191
|
+
type: 'mousePressed',
|
|
192
|
+
x: x,
|
|
193
|
+
y: y,
|
|
194
|
+
button: 'left',
|
|
195
|
+
clickCount: 1
|
|
196
|
+
});
|
|
197
|
+
await session.Input.dispatchMouseEvent({
|
|
198
|
+
type: 'mouseReleased',
|
|
199
|
+
x: x,
|
|
200
|
+
y: y,
|
|
201
|
+
button: 'left',
|
|
202
|
+
clickCount: 1
|
|
203
|
+
});
|
|
204
|
+
// Wait a bit for any navigation or updates
|
|
205
|
+
await setTimeout(100);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Type text into an input field
|
|
209
|
+
*/
|
|
210
|
+
export async function typeText(session, selector, text) {
|
|
211
|
+
const nodeId = await waitForSelector(session, selector);
|
|
212
|
+
// Focus the element
|
|
213
|
+
await session.DOM.focus({ nodeId });
|
|
214
|
+
// Clear existing value using nodeId
|
|
215
|
+
const result = await session.DOM.resolveNode({ nodeId });
|
|
216
|
+
if (result.object.objectId) {
|
|
217
|
+
await session.Runtime.callFunctionOn({
|
|
218
|
+
objectId: result.object.objectId,
|
|
219
|
+
functionDeclaration: 'function() { this.value = ""; this.focus(); }'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Type character by character
|
|
223
|
+
for (const char of text) {
|
|
224
|
+
await session.Input.dispatchKeyEvent({
|
|
225
|
+
type: 'char',
|
|
226
|
+
text: char
|
|
227
|
+
});
|
|
228
|
+
await setTimeout(10); // Small delay between keystrokes
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get text content of an element
|
|
233
|
+
*/
|
|
234
|
+
export async function getTextContent(session, selector) {
|
|
235
|
+
const nodeId = await waitForSelector(session, selector);
|
|
236
|
+
const result = await session.DOM.resolveNode({ nodeId });
|
|
237
|
+
if (result.object.objectId) {
|
|
238
|
+
const textResult = await session.Runtime.callFunctionOn({
|
|
239
|
+
objectId: result.object.objectId,
|
|
240
|
+
functionDeclaration: 'function() { return this.textContent || this.innerText || ""; }',
|
|
241
|
+
returnByValue: true
|
|
242
|
+
});
|
|
243
|
+
return textResult.result.value || '';
|
|
244
|
+
}
|
|
245
|
+
return '';
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if element is visible
|
|
249
|
+
*/
|
|
250
|
+
export async function isVisible(session, selector) {
|
|
251
|
+
try {
|
|
252
|
+
const nodeId = await querySelector(session, selector);
|
|
253
|
+
if (nodeId === null)
|
|
254
|
+
return false;
|
|
255
|
+
const result = await session.DOM.resolveNode({ nodeId });
|
|
256
|
+
if (!result.object.objectId)
|
|
257
|
+
return false;
|
|
258
|
+
const visibilityResult = await session.Runtime.callFunctionOn({
|
|
259
|
+
objectId: result.object.objectId,
|
|
260
|
+
functionDeclaration: `
|
|
261
|
+
function() {
|
|
262
|
+
const style = window.getComputedStyle(this);
|
|
263
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
264
|
+
}
|
|
265
|
+
`,
|
|
266
|
+
returnByValue: true
|
|
267
|
+
});
|
|
268
|
+
return visibilityResult.result.value === true;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Wait for specified seconds
|
|
276
|
+
*/
|
|
277
|
+
export async function wait(seconds) {
|
|
278
|
+
await setTimeout(seconds * 1000);
|
|
279
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Executor - Executes Assure commands
|
|
3
|
+
*/
|
|
4
|
+
import { executeOpen } from '../commands/open.js';
|
|
5
|
+
import { executeClick } from '../commands/click.js';
|
|
6
|
+
import { executeType } from '../commands/type.js';
|
|
7
|
+
import { executeExpect } from '../commands/expect.js';
|
|
8
|
+
import { wait } from './browser.js';
|
|
9
|
+
export async function execute(commands, session) {
|
|
10
|
+
for (const cmd of commands) {
|
|
11
|
+
const { action, args, lineNumber } = cmd;
|
|
12
|
+
try {
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'OPEN':
|
|
15
|
+
await executeOpen(session, args);
|
|
16
|
+
console.log(`โ Line ${lineNumber}: OPEN "${args[0]}"`);
|
|
17
|
+
break;
|
|
18
|
+
case 'CLICK':
|
|
19
|
+
await executeClick(session, args);
|
|
20
|
+
console.log(`โ Line ${lineNumber}: CLICK "${args[0]}"`);
|
|
21
|
+
break;
|
|
22
|
+
case 'TYPE':
|
|
23
|
+
await executeType(session, args);
|
|
24
|
+
console.log(`โ Line ${lineNumber}: TYPE "${args[0]}" "${args.slice(1).join(' ')}"`);
|
|
25
|
+
break;
|
|
26
|
+
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]}`);
|
|
30
|
+
}
|
|
31
|
+
await wait(seconds);
|
|
32
|
+
console.log(`โ Line ${lineNumber}: WAIT ${seconds}`);
|
|
33
|
+
break;
|
|
34
|
+
case 'EXPECT':
|
|
35
|
+
await executeExpect(session, args);
|
|
36
|
+
console.log(`โ Line ${lineNumber}: EXPECT ${args.join(' ')}`);
|
|
37
|
+
break;
|
|
38
|
+
case 'TEST':
|
|
39
|
+
// TEST is just a label, skip execution
|
|
40
|
+
console.log(`\n๐งช ${args.join(' ')}`);
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unknown command: ${action} at line ${lineNumber}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(`\nโ Error at line ${lineNumber}: ${action} ${args.join(' ')}`);
|
|
48
|
+
console.error(` ${error.message}`);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser - Parses Assure test scripts into command arrays
|
|
3
|
+
*/
|
|
4
|
+
import { tokenize } from './tokenizer.js';
|
|
5
|
+
export function parse(script) {
|
|
6
|
+
const lines = script.split('\n');
|
|
7
|
+
const commands = [];
|
|
8
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9
|
+
const line = lines[i].trim();
|
|
10
|
+
// Skip empty lines and comments
|
|
11
|
+
if (!line || line.startsWith('#')) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const tokens = tokenize(line);
|
|
15
|
+
if (tokens.length === 0) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const [action, ...args] = tokens;
|
|
19
|
+
commands.push({
|
|
20
|
+
action: action.toUpperCase(),
|
|
21
|
+
args: args.map(arg => arg.replace(/^["']|["']$/g, '')), // Remove quotes
|
|
22
|
+
lineNumber: i + 1
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenizer - Breaks lines into tokens
|
|
3
|
+
* Handles quoted strings and whitespace-separated tokens
|
|
4
|
+
*/
|
|
5
|
+
export function tokenize(line) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let current = '';
|
|
8
|
+
let inQuotes = false;
|
|
9
|
+
let quoteChar = '';
|
|
10
|
+
for (let i = 0; i < line.length; i++) {
|
|
11
|
+
const char = line[i];
|
|
12
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
13
|
+
inQuotes = true;
|
|
14
|
+
quoteChar = char;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (char === quoteChar && inQuotes) {
|
|
18
|
+
inQuotes = false;
|
|
19
|
+
if (current.trim()) {
|
|
20
|
+
tokens.push(current.trim());
|
|
21
|
+
current = '';
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (inQuotes) {
|
|
26
|
+
current += char;
|
|
27
|
+
}
|
|
28
|
+
else if (char === ' ' || char === '\t') {
|
|
29
|
+
if (current.trim()) {
|
|
30
|
+
tokens.push(current.trim());
|
|
31
|
+
current = '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
current += char;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (current.trim()) {
|
|
39
|
+
tokens.push(current.trim());
|
|
40
|
+
}
|
|
41
|
+
return tokens;
|
|
42
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assure Test Runner
|
|
3
|
+
* Runs .assure test files using custom CDP-based engine
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { parse } from './language/parser.js';
|
|
7
|
+
import { createBrowser, closeBrowser } from './engine/browser.js';
|
|
8
|
+
import { execute } from './engine/executor.js';
|
|
9
|
+
async function runTest(testFile) {
|
|
10
|
+
console.log(`\n๐ Assure Test Runner`);
|
|
11
|
+
console.log(`๐ Running: ${testFile}\n`);
|
|
12
|
+
// Validate file extension
|
|
13
|
+
if (!testFile.endsWith('.assure')) {
|
|
14
|
+
console.error(`โ Error: Test files must have .assure extension`);
|
|
15
|
+
console.error(` Received: ${testFile}`);
|
|
16
|
+
console.error(` Expected: *.assure`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
// Read test file
|
|
20
|
+
if (!fs.existsSync(testFile)) {
|
|
21
|
+
console.error(`โ Test file not found: ${testFile}`);
|
|
22
|
+
console.error(` Make sure the file exists and has the .assure extension`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const script = fs.readFileSync(testFile, 'utf-8');
|
|
26
|
+
const commands = parse(script);
|
|
27
|
+
if (commands.length === 0) {
|
|
28
|
+
console.error('โ No commands found in test file');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
let session;
|
|
32
|
+
try {
|
|
33
|
+
// Create browser session
|
|
34
|
+
console.log('๐ Launching browser...');
|
|
35
|
+
session = await createBrowser(true); // headless mode
|
|
36
|
+
console.log('โ Browser launched\n');
|
|
37
|
+
// Execute commands
|
|
38
|
+
await execute(commands, session);
|
|
39
|
+
console.log('\nโ
TEST COMPLETED SUCCESSFULLY');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error(`\nโ TEST FAILED: ${error.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
// Cleanup
|
|
47
|
+
if (session) {
|
|
48
|
+
console.log('\n๐งน Cleaning up...');
|
|
49
|
+
await closeBrowser(session);
|
|
50
|
+
console.log('โ Browser closed');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Main entry point
|
|
55
|
+
const testFile = process.argv[2];
|
|
56
|
+
if (!testFile) {
|
|
57
|
+
console.log(`
|
|
58
|
+
๐งช Assure Testing Language
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
assure <test-file.assure>
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
assure login.assure
|
|
65
|
+
assure tests/checkout.assure
|
|
66
|
+
|
|
67
|
+
For more information, visit: https://github.com/yourusername/assure
|
|
68
|
+
`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
runTest(testFile).catch((error) => {
|
|
72
|
+
console.error('Fatal error:', error);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "assure-testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Assure - A custom testing language (DSL) for browser automation. Test files use .assure extension.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/runner.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"assure": "./dist/runner.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "tsx runner.ts",
|
|
17
|
+
"run": "tsx runner.ts",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"start": "node dist/runner.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"testing",
|
|
24
|
+
"test",
|
|
25
|
+
"dsl",
|
|
26
|
+
"browser-automation",
|
|
27
|
+
"cdp",
|
|
28
|
+
"custom-engine",
|
|
29
|
+
"e2e",
|
|
30
|
+
"end-to-end",
|
|
31
|
+
"automation",
|
|
32
|
+
"chrome-devtools-protocol",
|
|
33
|
+
"assure"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/yourusername/assure.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/yourusername/assure/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/yourusername/assure#readme",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"chrome-remote-interface": "^0.33.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"tsx": "^4.7.0",
|
|
54
|
+
"typescript": "^5.3.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|