@vatzzza/botintern 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.
@@ -0,0 +1,418 @@
1
+ import fs from 'fs';
2
+ import yaml from 'js-yaml';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { spawn } from 'child_process';
6
+ import net from 'net';
7
+
8
+ // --- 1. HELPER: Normalize Actions (FIXED PRIORITY) ---
9
+ function normalizeAction(step) {
10
+ // 1. PRIORITY CHECK: Smart Type Shorthand
11
+ // We MUST check this first because the key is named 'type', which confuses the logic below.
12
+ if (step.type && step.into) {
13
+ return { type: 'type_smart', value: step.type, label: step.into };
14
+ }
15
+
16
+ // 2. Legacy/Developer Syntax (Explicit 'type' key)
17
+ if (step.type) {
18
+ // If it uses "selector", it's the old developer syntax.
19
+ // We rename it to 'type_selector' to distinguish it from the smart syntax.
20
+ if (step.type === 'type' && step.selector) {
21
+ return { type: 'type_selector', selector: step.selector, value: step.value };
22
+ }
23
+ // Return other explicit types (assert_text, assert_visible) as is
24
+ return step;
25
+ }
26
+
27
+ // 3. Product Manager Shorthands
28
+ if (step.see) return { type: 'see', value: step.see };
29
+ if (step.click) return { type: 'click', value: step.click };
30
+ if (step.wait) return { type: 'wait', ms: step.wait };
31
+ if (step.url) return { type: 'assert_url', value: step.url };
32
+
33
+ // 4. Color Assertions
34
+ if (step.color) {
35
+ const parts = step.color.split(/\s+on\s+/i);
36
+ if (parts.length === 2) {
37
+ return { type: 'assert_color', color: parts[0].trim(), element: parts[1].trim() };
38
+ }
39
+ }
40
+ if (step.background) {
41
+ const parts = step.background.split(/\s+on\s+/i);
42
+ if (parts.length === 2) {
43
+ return { type: 'assert_background', color: parts[0].trim(), element: parts[1].trim() };
44
+ }
45
+ }
46
+ if (step['border-color']) {
47
+ const parts = step['border-color'].split(/\s+on\s+/i);
48
+ if (parts.length === 2) {
49
+ return { type: 'assert_border_color', color: parts[0].trim(), element: parts[1].trim() };
50
+ }
51
+ }
52
+
53
+ // 5. Network Syntax
54
+ if (step.network) {
55
+ const parts = step.network.split(' ');
56
+ return { type: 'network_listen', method: parts[0] || 'GET', urlPart: parts[1] || parts[0] };
57
+ }
58
+
59
+ return step;
60
+ }
61
+
62
+ // --- 1.5. HELPER: Normalize Colors ---
63
+ function normalizeColor(color) {
64
+ // Convert any color format to RGB for comparison
65
+ const namedColors = {
66
+ 'red': 'rgb(255, 0, 0)',
67
+ 'blue': 'rgb(0, 0, 255)',
68
+ 'green': 'rgb(0, 128, 0)',
69
+ 'white': 'rgb(255, 255, 255)',
70
+ 'black': 'rgb(0, 0, 0)',
71
+ 'yellow': 'rgb(255, 255, 0)',
72
+ 'gray': 'rgb(128, 128, 128)',
73
+ 'grey': 'rgb(128, 128, 128)',
74
+ 'orange': 'rgb(255, 165, 0)',
75
+ 'purple': 'rgb(128, 0, 128)',
76
+ 'pink': 'rgb(255, 192, 203)',
77
+ 'brown': 'rgb(165, 42, 42)',
78
+ 'cyan': 'rgb(0, 255, 255)',
79
+ 'magenta': 'rgb(255, 0, 255)',
80
+ 'lime': 'rgb(0, 255, 0)',
81
+ 'navy': 'rgb(0, 0, 128)',
82
+ 'teal': 'rgb(0, 128, 128)',
83
+ 'silver': 'rgb(192, 192, 192)',
84
+ 'gold': 'rgb(255, 215, 0)',
85
+ };
86
+
87
+ const trimmed = color.trim().toLowerCase();
88
+
89
+ // Handle named colors
90
+ if (namedColors[trimmed]) {
91
+ return namedColors[trimmed];
92
+ }
93
+
94
+ // Handle hex colors (#fff, #ffffff, #ffffffff)
95
+ if (trimmed.startsWith('#')) {
96
+ let hex = trimmed.slice(1);
97
+
98
+ // Expand 3-digit hex to 6-digit
99
+ if (hex.length === 3) {
100
+ hex = hex.split('').map(c => c + c).join('');
101
+ }
102
+
103
+ // Parse RGB values
104
+ if (hex.length === 6 || hex.length === 8) {
105
+ const r = parseInt(hex.slice(0, 2), 16);
106
+ const g = parseInt(hex.slice(2, 4), 16);
107
+ const b = parseInt(hex.slice(4, 6), 16);
108
+ return `rgb(${r}, ${g}, ${b})`;
109
+ }
110
+ }
111
+
112
+ // Already in rgb/rgba format - normalize spacing
113
+ if (trimmed.startsWith('rgb')) {
114
+ // Extract just rgb values, ignore alpha
115
+ const match = trimmed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
116
+ if (match) {
117
+ return `rgb(${match[1]}, ${match[2]}, ${match[3]})`;
118
+ }
119
+ }
120
+
121
+ return color;
122
+ }
123
+
124
+ // --- 2. HELPER: Wait for Server ---
125
+ function waitForServer(port = 3000, timeout = 30000) {
126
+ return new Promise((resolve, reject) => {
127
+ const start = Date.now();
128
+ const interval = setInterval(() => {
129
+ const socket = new net.Socket();
130
+ socket.setTimeout(1000);
131
+ socket.on('connect', () => { socket.destroy(); clearInterval(interval); resolve(); });
132
+ socket.on('error', () => {
133
+ socket.destroy();
134
+ if (Date.now() - start > timeout) {
135
+ clearInterval(interval);
136
+ reject(new Error('Server timeout'));
137
+ }
138
+ });
139
+ socket.connect(port, 'localhost');
140
+ }, 1000);
141
+ });
142
+ }
143
+
144
+ // --- 3. MAIN ENGINE ---
145
+ export async function runTests(page) {
146
+ console.log(chalk.bold.blue('\n🚀 Starting Vibe Verification Engine...\n'));
147
+
148
+ // A. Load YAML
149
+ const cwd = process.cwd();
150
+ const yamlPath = `${cwd}/vibe.yaml`;
151
+ let plan = "";
152
+
153
+ if (fs.existsSync(yamlPath)) {
154
+ try {
155
+ const fileContents = fs.readFileSync(yamlPath, 'utf8');
156
+ plan = yaml.load(fileContents);
157
+ } catch (e) {
158
+ console.log(chalk.red(`❌ Error reading vibe.yaml: ${e.message}`));
159
+ return { success: false, error: "Error reading vibe.yaml" };
160
+ }
161
+ } else {
162
+ console.log(chalk.red(`❌ Could not find "vibe.yaml" in: ${cwd}`));
163
+ console.log(chalk.yellow(`💡 Make sure you're running this command from the project directory containing vibe.yaml`));
164
+ return { success: false, error: "Missing vibe.yaml" };
165
+ }
166
+
167
+ const baseUrl = plan.meta.baseUrl || 'http://localhost:3000';
168
+ const failureReport = []; // Store failures here
169
+ let totalTests = 0; // Track total number of tests
170
+ let passedTests = 0; // Track passed tests
171
+
172
+ // B. Smart Server Startup
173
+ let spinner = null; // Initialize spinner before any usage
174
+ try {
175
+ await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 3000 });
176
+ } catch (e) {
177
+ console.log(chalk.yellow('⚠️ Localhost is down. Auto-starting...'));
178
+ spinner = ora('Booting up server...').start();
179
+ const child = spawn('npm', ['run', 'dev'], { stdio: 'ignore', detached: true, shell: true });
180
+ child.unref();
181
+
182
+ try {
183
+ await waitForServer(3000);
184
+ spinner.succeed(chalk.green('Server is up!'));
185
+ spinner = null;
186
+ await page.goto(baseUrl, { waitUntil: 'networkidle' });
187
+ } catch (startError) {
188
+ spinner.fail(chalk.red('Could not auto-start server.'));
189
+ spinner = null;
190
+ return { success: false, error: "Server failed to start" };
191
+ }
192
+ }
193
+
194
+ // C. The Testing Loop
195
+ let pendingNetworkPromise = null;
196
+
197
+ for (const scenario of plan.scenarios) {
198
+ console.log(chalk.dim('-----------------------------------'));
199
+ console.log(chalk.bold.magenta(`Testing Scenario: ${scenario.name}`));
200
+
201
+ spinner = ora(`Navigating to ${scenario.path}...`).start();
202
+ try {
203
+ await page.goto(`${baseUrl}${scenario.path}`, { waitUntil: 'networkidle' });
204
+ spinner.succeed(`Arrived at ${scenario.path}`);
205
+ spinner = null;
206
+ } catch (e) {
207
+ spinner.fail(`Could not load ${scenario.path}`);
208
+ spinner = null;
209
+ failureReport.push({
210
+ scenario: scenario.name,
211
+ step: "Navigation",
212
+ error: `Could not load ${scenario.path}`
213
+ });
214
+ continue;
215
+ }
216
+
217
+ for (const rawStep of scenario.tests) {
218
+ const action = normalizeAction(rawStep);
219
+ spinner = ora(`Checking...`).start();
220
+
221
+ try {
222
+ // If we have a pending network listener, wait for it unless we are about to trigger it
223
+ if (pendingNetworkPromise && action.type !== 'click' && action.type !== 'type_smart') {
224
+ spinner.text = "Waiting for previous API call...";
225
+ await pendingNetworkPromise;
226
+ pendingNetworkPromise = null;
227
+ }
228
+
229
+ // Skip network_listen from test count as it's a setup action
230
+ if (action.type !== 'network_listen') {
231
+ totalTests++;
232
+ }
233
+
234
+ switch (action.type) {
235
+ case 'see':
236
+ spinner.text = `Looking for text: "${action.value}"`;
237
+ if (await page.getByText(action.value).isVisible()) {
238
+ spinner.succeed(chalk.green(`✅ Saw: "${action.value}"`));
239
+ passedTests++;
240
+ } else {
241
+ throw new Error(`Text "${action.value}" not found.`);
242
+ }
243
+ break;
244
+
245
+ case 'assert_visible':
246
+ await page.waitForSelector(action.selector, { state: 'visible', timeout: 5000 });
247
+ spinner.succeed(chalk.green(`✅ Visible: ${action.selector}`));
248
+ passedTests++;
249
+ break;
250
+
251
+ case 'assert_text':
252
+ const el = await page.waitForSelector(action.selector);
253
+ const txt = await el.textContent();
254
+ if (txt.includes(action.value)) {
255
+ spinner.succeed(chalk.green(`✅ Text Match: "${action.value}"`));
256
+ passedTests++;
257
+ } else {
258
+ throw new Error(`Expected "${action.value}", found "${txt}"`);
259
+ }
260
+ break;
261
+
262
+ case 'click':
263
+ spinner.text = `Clicking "${action.value}"...`;
264
+ const clickAction = page.click(`text=${action.value}`, { timeout: 5000 });
265
+ if (pendingNetworkPromise) {
266
+ await Promise.all([pendingNetworkPromise, clickAction]);
267
+ pendingNetworkPromise = null;
268
+ spinner.succeed(`🖱️ Clicked "${action.value}" & ✅ API Verified`);
269
+ } else {
270
+ await clickAction;
271
+ spinner.succeed(`🖱️ Clicked "${action.value}"`);
272
+ }
273
+ passedTests++;
274
+ break;
275
+
276
+ case 'type_smart':
277
+ spinner.text = `Typing into "${action.label}"...`;
278
+ // Try Label first (best practice), then Placeholder (fallback)
279
+ const labelMatch = page.getByLabel(action.label);
280
+ const placeholderMatch = page.getByPlaceholder(action.label);
281
+
282
+ if (await labelMatch.count() > 0) await labelMatch.fill(action.value);
283
+ else if (await placeholderMatch.count() > 0) await placeholderMatch.fill(action.value);
284
+ else throw new Error(`Input "${action.label}" not found.`);
285
+
286
+ spinner.succeed(`⌨️ Typed "${action.value}"`);
287
+ passedTests++;
288
+ break;
289
+
290
+ case 'type_selector':
291
+ await page.fill(action.selector, action.value);
292
+ spinner.succeed(`⌨️ Typed into ${action.selector}`);
293
+ passedTests++;
294
+ break;
295
+
296
+ case 'assert_url':
297
+ spinner.text = `Verifying URL...`;
298
+ await page.waitForURL(`**${action.value}**`, { timeout: 5000 });
299
+ spinner.succeed(`📍 Navigated to "${action.value}"`);
300
+ passedTests++;
301
+ break;
302
+
303
+ case 'network_listen':
304
+ spinner.text = `👂 Listening for ${action.method} ${action.urlPart}...`;
305
+ pendingNetworkPromise = page.waitForResponse(res =>
306
+ res.url().includes(action.urlPart) &&
307
+ res.request().method() === action.method &&
308
+ res.status() === 200
309
+ );
310
+ spinner.info(`👂 Listening for ${action.method} ${action.urlPart}...`);
311
+ break;
312
+
313
+ case 'assert_color':
314
+ spinner.text = `Checking text color of "${action.element}"...`;
315
+ try {
316
+ const colorElement = await page.getByText(action.element).first();
317
+ const actualColor = await colorElement.evaluate(el =>
318
+ window.getComputedStyle(el).color
319
+ );
320
+ const expectedColor = normalizeColor(action.color);
321
+ const normalizedActual = normalizeColor(actualColor);
322
+
323
+ if (normalizedActual === expectedColor) {
324
+ spinner.succeed(chalk.green(`✅ Color matches: ${action.color}`));
325
+ passedTests++;
326
+ } else {
327
+ throw new Error(`Expected color "${action.color}" (${expectedColor}) but got "${actualColor}" (${normalizedActual})`);
328
+ }
329
+ } catch (error) {
330
+ throw new Error(`Color check failed for "${action.element}": ${error.message}`);
331
+ }
332
+ break;
333
+
334
+ case 'assert_background':
335
+ spinner.text = `Checking background color of "${action.element}"...`;
336
+ try {
337
+ const bgElement = await page.getByText(action.element).first();
338
+ const actualBg = await bgElement.evaluate(el =>
339
+ window.getComputedStyle(el).backgroundColor
340
+ );
341
+ const expectedBg = normalizeColor(action.color);
342
+ const normalizedBg = normalizeColor(actualBg);
343
+
344
+ if (normalizedBg === expectedBg) {
345
+ spinner.succeed(chalk.green(`✅ Background matches: ${action.color}`));
346
+ passedTests++;
347
+ } else {
348
+ throw new Error(`Expected background "${action.color}" (${expectedBg}) but got "${actualBg}" (${normalizedBg})`);
349
+ }
350
+ } catch (error) {
351
+ throw new Error(`Background check failed for "${action.element}": ${error.message}`);
352
+ }
353
+ break;
354
+
355
+ case 'assert_border_color':
356
+ spinner.text = `Checking border color of "${action.element}"...`;
357
+ try {
358
+ const borderElement = await page.getByText(action.element).first();
359
+ const actualBorder = await borderElement.evaluate(el =>
360
+ window.getComputedStyle(el).borderColor
361
+ );
362
+ const expectedBorder = normalizeColor(action.color);
363
+ const normalizedBorder = normalizeColor(actualBorder);
364
+
365
+ if (normalizedBorder === expectedBorder) {
366
+ spinner.succeed(chalk.green(`✅ Border color matches: ${action.color}`));
367
+ passedTests++;
368
+ } else {
369
+ throw new Error(`Expected border color "${action.color}" (${expectedBorder}) but got "${actualBorder}" (${normalizedBorder})`);
370
+ }
371
+ } catch (error) {
372
+ throw new Error(`Border color check failed for "${action.element}": ${error.message}`);
373
+ }
374
+ break;
375
+
376
+ case 'wait':
377
+ await page.waitForTimeout(action.ms);
378
+ spinner.succeed(`zzz Waited ${action.ms}ms`);
379
+ passedTests++;
380
+ break;
381
+
382
+ default:
383
+ spinner.warn(`⚠️ Unknown Action: ${JSON.stringify(action)}`);
384
+ }
385
+
386
+ // Clean up spinner after successful test step
387
+ spinner = null;
388
+
389
+ } catch (error) {
390
+ spinner.fail(chalk.red(`❌ FAILED: ${JSON.stringify(action)}`));
391
+ spinner = null;
392
+ failureReport.push({
393
+ scenario: scenario.name,
394
+ action: action,
395
+ error: error.message
396
+ });
397
+ pendingNetworkPromise = null;
398
+ }
399
+ }
400
+ }
401
+
402
+ console.log(chalk.dim('\n-----------------------------------'));
403
+
404
+ // Check if tests actually ran
405
+ if (totalTests === 0) {
406
+ console.log(chalk.yellow.bold('⚠️ No tests were executed. Please check your vibe.yaml file.'));
407
+ return { success: false, error: "No tests executed" };
408
+ }
409
+
410
+ if (failureReport.length === 0 && passedTests === totalTests) {
411
+ console.log(chalk.green.bold(`✨ All Vibes Passed! ${passedTests}/${totalTests} tests successful.`));
412
+ return { success: true, totalTests, passedTests };
413
+ } else {
414
+ console.log(chalk.red.bold(`🚫 Vibe Check Failed! ${failureReport.length} failures out of ${totalTests} tests.`));
415
+ console.log(chalk.yellow(` Passed: ${passedTests}/${totalTests}`));
416
+ return { success: false, failures: failureReport, totalTests, passedTests };
417
+ }
418
+ }
package/lib/scan.js ADDED
@@ -0,0 +1,58 @@
1
+ // NOT USED ANYWHERE YET. NEEDS MORE REFINEMENT.
2
+
3
+ import chalk from "chalk";
4
+ import exec from "child_process";
5
+ import ora from "ora";
6
+ import { stdout } from "process";
7
+
8
+ async function scanBuildErrors() {
9
+ console.log(chalk.green('🌊 Vibe Guard Initialized...'));
10
+
11
+ // Start the spinner (The event loop needs to be free for this to spin!)
12
+ const spinner = ora(chalk.yellow('👀 Looking for Hallucinations (Running build)...')).start();
13
+ let result = ""
14
+
15
+ try {
16
+ // We use 'await' here. This allows the spinner to keep spinning
17
+ // while the build runs in the background.
18
+ const { stdout, stderr } = exec.exec("npm run build", {
19
+ encoding: 'utf8',
20
+ maxBuffer: 50 * 1024 * 1024
21
+ });
22
+
23
+ // If we get here, the exit code was 0 (Success)
24
+ spinner.succeed(chalk.green('Build Passed! No errors found.'));
25
+ result = stdout + "\n" + stderr;
26
+ return result;
27
+
28
+ } catch (error) {
29
+ // If 'npm run build' fails (exit code 1), it throws an error.
30
+ // We catch it here.
31
+ spinner.fail(chalk.red('Build Failed! Hallucinations detected.'));
32
+
33
+ console.log('');
34
+ console.log(chalk.dim('--- Error Logs ---'));
35
+
36
+ // In the catch block, the output is inside error.stdout / error.stderr
37
+ const logs = (error.stdout || "") + "\n" + (error.stderr || "");
38
+ console.log(error);
39
+ // result = stdout + "\n" + stderr;
40
+ return logs;
41
+ }
42
+
43
+ // function (error, stdout, stderr) {
44
+ // if (error) {
45
+ // spinner.fail(chalk.red('Build Failed! Hallucinations detected.'));
46
+ // console.log('');
47
+ // console.log(chalk.dim('--- Error Logs ---'));
48
+ // console.log(stdout);
49
+ // console.log(stderr);
50
+ // return stdout + "\n" + stderr;
51
+ // } else {
52
+ // spinner.succeed(chalk.green('Build Passed! No errors found.'));
53
+ // return "";
54
+ // }
55
+ // }
56
+ }
57
+
58
+ export { scanBuildErrors };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@vatzzza/botintern",
3
+ "version": "1.0.0",
4
+ "module": "index.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "botintern": "./index.js"
8
+ },
9
+ "devDependencies": {
10
+ "@types/bun": "latest"
11
+ },
12
+ "peerDependencies": {
13
+ "typescript": "^5"
14
+ },
15
+ "dependencies": {
16
+ "@google/genai": "^1.38.0",
17
+ "@google/generative-ai": "^0.24.1",
18
+ "chalk": "^5.6.2",
19
+ "commander": "^14.0.2",
20
+ "dotenv": "^17.2.3",
21
+ "js-yaml": "^4.1.1",
22
+ "ora": "^9.1.0",
23
+ "playwright": "^1.58.0",
24
+ "yocto-spinner": "^1.0.0"
25
+ },
26
+ "scripts": {
27
+ "dev": "bun --watch index.js"
28
+ }
29
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }
@@ -0,0 +1,33 @@
1
+ // lib/utils.js
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Removes weird terminal colors (ANSI codes) so Regex works
6
+ */
7
+ function stripAnsi(string) {
8
+ return string.replace(
9
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
10
+ ''
11
+ );
12
+ }
13
+
14
+ /**
15
+ * Hunts through the logs to find the first broken file.
16
+ * Returns null if no file is found.
17
+ */
18
+ export function extractErrorFile(logs) {
19
+ const cleanLogs = stripAnsi(logs);
20
+
21
+ // REGEX: Looks for paths starting with ./ followed by common folders
22
+ // Captures: ./app/page.tsx, ./src/components/Button.tsx, etc.
23
+ const regex = /(\.\/(?:app|src|components|pages|lib)\/[a-zA-Z0-9_\-\/]+\.(tsx|ts|jsx|js))/i;
24
+
25
+ const match = cleanLogs.match(regex);
26
+
27
+ if (match && match[1]) {
28
+ // Return the clean relative path (e.g., "./app/page.tsx")
29
+ return match[1];
30
+ }
31
+
32
+ return null;
33
+ }
@@ -0,0 +1,29 @@
1
+ import exec from "child_process";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+
5
+ function setupPlaywright() {
6
+ const spinner = ora(chalk.green('Installing @playwright/test...')).start();
7
+ exec.exec("npm i playwright", function (error, stdout, stderr) {
8
+ console.log(stdout);
9
+ console.log(stderr);
10
+ if (error) {
11
+ spinner.fail(chalk.red('Installing @playwright/test...'));
12
+ console.error(error);
13
+ } else {
14
+ spinner.succeed(chalk.green('Installing @playwright/test...'));
15
+ }
16
+ });
17
+ exec.exec("npx playwright install", function (error, stdout, stderr) {
18
+ console.log(stdout);
19
+ console.log(stderr);
20
+ if (error) {
21
+ spinner.fail(chalk.red('Installing playwright...'));
22
+ console.error(error);
23
+ } else {
24
+ spinner.succeed(chalk.green('Installing playwright...'));
25
+ }
26
+ });
27
+ }
28
+
29
+ export { setupPlaywright };
@@ -0,0 +1,21 @@
1
+ meta:
2
+ appName: "My Next.js App"
3
+ baseUrl: "http://localhost:3000"
4
+
5
+ scenarios:
6
+ - name: "About Page Check"
7
+ path: "/about"
8
+ tests:
9
+ # 1. Check if we actually landed on the page (Look for the main header)
10
+ - name: "Check if we actually landed on the page (Look for the main header)"
11
+ type: "assert_visible"
12
+ selector: "h1"
13
+
14
+ # 2. strict check: The h1 should say "About"
15
+ - type: "assert_text"
16
+ selector: "h1"
17
+ value: "About"
18
+
19
+ # 3. (Optional) Check for other common elements like a nav bar
20
+ - type: "assert_visible"
21
+ selector: "nav"