panoptical
Version:
A modern testing tool that makes Playwright easy to use with simple YAML syntax
476 lines (399 loc) • 17.9 kB
JavaScript
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import YAML from 'yaml';
import { PanopticalCompatibility } from '../browser/compatibility.js';
import { ScreenshotManager } from '../utils/screenshots.js';
/**
* Convert milliseconds to human-readable format
* @param {number} ms - Milliseconds
* @returns {string} Formatted time string
*/
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = ms / 1000;
if (seconds < 60) {
// Show seconds with 1 decimal place for precision
return `${seconds.toFixed(1)}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (remainingSeconds < 1) {
return `${minutes}m`;
}
// Show minutes and seconds with 1 decimal place
return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
}
export async function runDeclarativeTest(filePath, options = {}) {
if (!fs.existsSync(filePath)) {
throw new Error(`Test file not found: ${filePath}`);
}
const raw = fs.readFileSync(filePath, 'utf8');
let data;
try {
data = YAML.parse(raw);
} catch (e) {
throw new Error(`Invalid YAML in ${filePath}: ${e}`);
}
if (!data.test || !data.steps || !Array.isArray(data.steps)) {
throw new Error(`Invalid test structure in ${filePath}. Must have 'test' and 'steps' fields.`);
}
console.log(`Running test: ${data.test}`);
if (data.description) {
console.log(`Description: ${data.description}`);
}
// Initialize screenshot manager
const screenshotManager = new ScreenshotManager();
// Use the new Panoptical browser engine
const browser = new PanopticalCompatibility(options);
const startTime = Date.now();
try {
await browser.launch();
await browser.newPage();
// Run setup steps if defined
if (data.setup && Array.isArray(data.setup)) {
console.log('Running setup steps...');
await runSteps(browser, data.setup, 'setup', screenshotManager, data.test);
}
// Run main test steps
console.log('Running test steps...');
await runSteps(browser, data.steps, 'test', screenshotManager, data.test);
// Run teardown steps if defined
if (data.teardown && Array.isArray(data.teardown)) {
console.log('Running teardown steps...');
await runSteps(browser, data.teardown, 'teardown', screenshotManager, data.test);
}
const duration = Date.now() - startTime;
console.log(`Test completed in ${formatDuration(duration)}`);
// Clean up video on success
if (browser.cleanupVideoOnSuccess) {
await browser.cleanupVideoOnSuccess();
}
// Close browser for successful tests
await browser.close();
return { success: true, duration };
} catch (error) {
const duration = Date.now() - startTime;
let screenshotInfo = '';
let videoInfo = '';
try {
const filename = path.basename(filePath, '.yaml');
const failureFilename = screenshotManager.generateTestFailureFilename(filename);
const failurePath = screenshotManager.getFailureScreenshotPath(failureFilename);
// Take screenshot and get buffer (without saving to file)
const screenshotBuffer = await browser.screenshot();
// Convert buffer to base64 for storage in JSON
const base64Screenshot = screenshotBuffer.toString('base64');
// Store the base64 data directly in the screenshotInfo
screenshotInfo = `Screenshot: ${base64Screenshot}`;
// Optionally save to file for local development (but don't use the return value)
try {
await fs.promises.writeFile(failurePath, screenshotBuffer);
} catch (writeError) {
console.warn('Could not save screenshot to file:', writeError.message);
}
} catch (screenshotError) {
console.error('Failed to take failure screenshot:', screenshotError.message);
}
try {
if (browser.saveVideoOnFailure) {
const filename = path.basename(filePath, '.yaml');
const videoPath = await browser.saveVideoOnFailure(filename);
if (videoPath) {
videoInfo = `Video: ${path.basename(videoPath)}`;
} else if (browser.options && browser.options.video && browser.options.video.enabled) {
videoInfo = 'Video: Failed to save';
}
} else {
videoInfo = 'Video: Not available';
}
} catch (videoError) {
console.error('Failed to save failure video:', videoError.message);
videoInfo = 'Video: Failed to save';
}
// Create enhanced error with screenshot and video info
const enhancedError = new Error(error.message);
enhancedError.screenshotInfo = screenshotInfo;
enhancedError.videoInfo = videoInfo;
enhancedError.originalError = error;
throw enhancedError;
} finally {
try {
if (browser.page && !browser.page.isClosed()) {
await browser.close();
}
} catch (closeError) {
}
}
}
async function runSteps(browser, steps, stepType, screenshotManager, testName) {
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
try {
if (step.goto) {
// Retry navigation up to 3 times
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
await browser.goto(step.goto);
break; // Success, exit retry loop
} catch (error) {
retryCount++;
if (retryCount >= maxRetries) {
throw error; // Give up after max retries
}
console.log(` Retry ${retryCount}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds before retry
}
}
// Use a more robust wait strategy
try {
await browser.waitForLoadState();
} catch (e) {
// Fallback: wait for body element (use 1/3 of browser timeout)
const fallbackTimeout = Math.floor(browser.browser.options.timeout / 3);
await browser.waitForSelector('body', fallbackTimeout);
}
// Navigation completed successfully
console.log(chalk.green(`✓ Navigated to `) + chalk.bold.green(`${step.goto}`));
}
if (step.click) {
if (typeof step.click === 'string') {
await browser.waitForSelector(step.click);
await browser.click(step.click);
console.log(chalk.green(`✓ Clicked on element `) + chalk.bold.green(`${step.click}`));
} else if (step.click.text) {
const timeout = step.click.timeout || browser.browser.options.timeout;
const page = browser.getPage();
await page.waitForFunction(
(searchText) => {
if (!document || !document.body) return false;
return document.body.innerText && document.body.innerText.includes(searchText);
},
step.click.text,
{ timeout }
);
await page.click(`text="${step.click.text}"`);
console.log(chalk.green(`✓ Clicked on text `) + chalk.bold.green(`"${step.click.text}"`));
} else if (step.click.selector) {
const timeout = step.click.timeout || browser.browser.options.timeout;
await browser.waitForSelector(step.click.selector, timeout);
await browser.click(step.click.selector);
console.log(chalk.green(`✓ Clicked on element `) + chalk.bold.green(`${step.click.selector}`));
} else {
throw new Error('click action must specify either a string selector, text, or selector object');
}
}
if (step.fill) {
await browser.waitForSelector(step.fill.selector);
await browser.fill(step.fill.selector, step.fill.text);
console.log(chalk.green(`✓ Filled `) + chalk.bold.green(`"${step.fill.text}"`) + chalk.green(` into `) + chalk.bold.green(`${step.fill.selector}`));
}
if (step.type) {
await browser.waitForSelector(step.type.selector);
const delay = step.type.delay || 100;
await browser.type(step.type.selector, step.type.text, { delay });
console.log(chalk.green(`✓ Typed `) + chalk.bold.green(`"${step.type.text}"`) + chalk.green(` into `) + chalk.bold.green(`${step.type.selector}`) + chalk.green(` with ${delay}ms delay`));
}
if (step.selectOption) {
await browser.waitForSelector(step.selectOption.selector);
await browser.selectOption(step.selectOption.selector, step.selectOption.value);
console.log(chalk.green(`✓ Selected `) + chalk.bold.green(`"${step.selectOption.value}"`) + chalk.green(` from `) + chalk.bold.green(`${step.selectOption.selector}`));
}
if (step.evaluate) {
await browser.waitForSelector(step.evaluate.selector);
await browser.evaluate(step.evaluate.script, step.evaluate.selector);
console.log(chalk.green(`✓ Executed script on `) + chalk.bold.green(`${step.evaluate.selector}`));
}
if (step.randomFill) {
await browser.randomFill(step.randomFill);
}
if (step.expect) {
await browser.waitForSelector(step.expect.selector);
const text = await browser.getText(step.expect.selector);
if (!text.includes(step.expect.text)) {
throw new Error(`Expected text "${step.expect.text}" not found. Got: "${text}"`);
}
console.log(chalk.green(`✓ Text `) + chalk.bold.green(`"${step.expect.text}"`) + chalk.green(` is verified`));
}
if (step.wait) {
if (step.wait.text) {
const timeout = step.wait.timeout || browser.browser.options.timeout;
const page = browser.getPage();
await page.waitForFunction(
(searchText) => {
if (!document || !document.body) return false;
return document.body.innerText && document.body.innerText.includes(searchText);
},
step.wait.text,
{ timeout }
);
console.log(chalk.green(`✓ Text `) + chalk.bold.green(`"${step.wait.text}"`) + chalk.green(` is found`));
} else if (step.wait.selector) {
const timeout = step.wait.timeout || browser.browser.options.timeout;
await browser.waitForSelector(step.wait.selector, timeout);
console.log(chalk.green(`✓ Element `) + chalk.bold.green(`${step.wait.selector}`) + chalk.green(` is found`));
} else {
throw new Error('wait action must specify either "text" or "selector"');
}
}
if (step.snapshot) {
const filename = screenshotManager.generateTestSuccessFilename(testName, step.snapshot);
const screenshotPath = screenshotManager.getSuccessScreenshotPath(filename);
await browser.screenshot(screenshotPath);
console.log(chalk.green(`✓ Screenshot `) + chalk.bold.green(`${filename}`) + chalk.green(` is taken`));
}
if (step.pause) {
await new Promise(resolve => setTimeout(resolve, step.pause));
console.log(chalk.green(`✓ Paused for `) + chalk.bold.green(`${step.pause}ms`));
}
// login - Logs in with username/password
if (step.login) {
await browser.login(step.login);
}
// logout - Logs out and verifies redirect
if (step.logout) {
const { logoutSelector, successIndicator } = step.logout;
await browser.logout(logoutSelector, successIndicator);
}
// goto_with_auth - Navigates with auth token
if (step.goto_with_auth) {
const { url, authToken, tokenHeader } = step.goto_with_auth;
await browser.gotoWithAuth(url, authToken, tokenHeader);
}
// click_if_visible - Clicks only if visible
if (step.click_if_visible) {
const { selector, options } = step.click_if_visible;
await browser.clickIfVisible(selector, options);
}
// select_from_dropdown - Selects dropdown option by text
if (step.select_from_dropdown) {
const { selector, optionText, options } = step.select_from_dropdown;
await browser.selectFromDropdown(selector, optionText, options);
}
// hover_and_click - Hover then click submenu
if (step.hover_and_click) {
const { hoverSelector, clickSelector, options } = step.hover_and_click;
await browser.hoverAndClick(hoverSelector, clickSelector, options);
}
// upload_file - Uploads and verifies file
if (step.upload_file) {
const { fileInputSelector, filePath, successIndicator, uploadButtonSelector } = step.upload_file;
await browser.uploadFile(fileInputSelector, filePath, successIndicator, uploadButtonSelector);
}
// download_and_verify - Downloads and verifies file
if (step.download_and_verify) {
const { downloadSelector, expectedSize, expectedContent } = step.download_and_verify;
await browser.downloadAndVerify(downloadSelector, expectedSize, expectedContent);
}
// take_screenshot - Takes named screenshot
if (step.take_screenshot) {
const { name, options } = step.take_screenshot;
await browser.takeScreenshot(name, options);
}
// verify_table_row - Verifies table contains specific row
if (step.verify_table_row) {
const { tableSelector, expectedRow } = step.verify_table_row;
await browser.verifyTableRow(tableSelector, expectedRow);
}
// assert_element_count - Asserts element count
if (step.assert_element_count) {
const { selector, expectedCount, operator } = step.assert_element_count;
await browser.assertElementCount(selector, expectedCount, operator);
}
// check_api_response - Checks API response
if (step.check_api_response) {
const { url, method, headers, body, expectedStatus, expectedContent } = step.check_api_response;
await browser.checkApiResponse(url, method, headers, body, expectedStatus, expectedContent);
}
// assert_element_not_present - Asserts element doesn't exist
if (step.assert_element_not_present) {
const { selector, timeout } = step.assert_element_not_present;
await browser.assertElementNotPresent(selector, timeout || browser.browser.options.timeout);
}
// measure_performance - Measures performance
if (step.measure_performance) {
const { action, options } = step.measure_performance;
await browser.measurePerformance(action, options);
}
// repeat - Repeats steps
if (step.repeat) {
const { steps: repeatSteps, count, options } = step.repeat;
await browser.repeat(repeatSteps, count, options);
}
// run_if - Runs steps conditionally
if (step.run_if) {
const { condition, steps: conditionalSteps, options } = step.run_if;
await browser.runIf(condition, conditionalSteps, options);
}
// store_text - Stores text in variable
if (step.store_text) {
const { selector, variableName } = step.store_text;
await browser.storeText(selector, variableName);
}
// compare_values - Compares values
if (step.compare_values) {
const { value1, value2, operator, options } = step.compare_values;
await browser.compareValues(value1, value2, operator, options);
}
// random_fill - Fills form with random data
if (step.random_fill) {
const { formData } = step.random_fill;
await browser.randomFill(formData);
}
// resize_viewport - Resizes viewport for responsive testing
if (step.resize_viewport) {
const { width, height, device } = step.resize_viewport;
await browser.resizeViewport(width, height, device);
}
// swipe - Performs swipe gesture
if (step.swipe) {
const { selector, direction, distance } = step.swipe;
await browser.swipe(selector, direction, distance);
}
// tap - Performs tap gesture
if (step.tap) {
const { selector, pressure } = step.tap;
await browser.tap(selector, pressure);
}
// drag_and_drop - Drags and drops elements
if (step.drag_and_drop) {
const { source, target } = step.drag_and_drop;
await browser.dragAndDrop(source, target);
}
// multi_select - Selects multiple options
if (step.multi_select) {
const { selector, options } = step.multi_select;
await browser.multiSelect(selector, options);
}
// press_keys - Presses keyboard keys
if (step.press_keys) {
const { keys, targetSelector } = step.press_keys;
await browser.pressKeys(keys, targetSelector);
}
// scroll_to_element - Scrolls to element
if (step.scroll_to_element) {
const { selector, behavior } = step.scroll_to_element;
await browser.scrollToElement(selector, behavior);
}
// hover_element - Hovers over element
if (step.hover_element) {
const { selector, duration } = step.hover_element;
await browser.hoverElement(selector, duration);
}
// iframe_action - Performs actions inside iframes
if (step.iframe_action) {
const { iframeSelector, action, targetSelector, options } = step.iframe_action;
await browser.iframeAction(iframeSelector, action, targetSelector, options);
}
} catch (error) {
const stepNumber = i + 1;
const stepAction = Object.keys(step)[0] || 'unknown';
const stepDetails = JSON.stringify(step[stepAction] || step);
throw new Error(`Step ${stepNumber} (${stepAction}): ${stepDetails}\nFailure: ${error.message || error}`);
}
}
}