@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
287 lines • 15.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Crawler = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const browser_1 = require("../selenium/browser");
/**
* Crawler class that extends Browser to provide crawling functionality for testing links and components.
*/
class Crawler extends browser_1.Browser {
/**
* Scans the current page to find and log custom component tags.
*
* It extracts the component name from the URL path, constructs a CSS selector
* using the `kendo-` prefix, and optionally checks for a singular form.
*
* Logs a warning if no matching elements are found or if a search fails.
*
* Special case: if the extracted name is "general", it uses the previous path segment instead.
*
* @param customTags - Optional array of custom CSS selectors to search for instead of URL-based extraction
*
* @example
* ```typescript
* // Use URL-based extraction (default behavior)
* await crawler.crawlForTags();
*
* // Use custom tags
* await crawler.crawlForTags(['kendo-grid', 'kendo-chart']);
*
* // Use complex selectors
* await crawler.crawlForTags(['kendo-grid .k-grid-header', 'kendo-button[type="submit"]']);
* ```
*/
crawlForTags(customTags) {
return __awaiter(this, void 0, void 0, function* () {
const currentUrl = yield this.getCurrentUrl();
let cssSelector;
if (customTags && customTags.length > 0) {
// Use provided custom tags
cssSelector = customTags.join(', ');
console.log(`Using Custom Tags: ${cssSelector}`);
}
else {
// Extract component name from URL (existing logic)
const segments = currentUrl.split('/').filter(Boolean); // remove empty parts
let extractedComponent = segments[segments.length - 2]; // default: second-to-last
if (extractedComponent === 'general') {
extractedComponent = segments[segments.length - 3]; // one level up
}
const expectedTag = `kendo-${extractedComponent}`;
const expectedTagSingular = extractedComponent.endsWith('s') ? `kendo-${extractedComponent.slice(0, -1)}` : null;
cssSelector = expectedTag;
if (expectedTagSingular) {
cssSelector = `${expectedTag}, ${expectedTagSingular}`;
}
}
try {
const elements = yield this.findAll(browser_1.By.css(cssSelector));
if (elements.length === 0) {
console.warn(`Warning during search for tags: No elements found with tags ${cssSelector} on page ${currentUrl}`);
}
else {
console.log(`Successful search for tags: Found ${elements.length} elements with tags ${cssSelector} on page ${currentUrl}`);
}
}
catch (error) {
console.warn(`Error during search for tags: Failed to find elements with tags ${cssSelector}:`, error);
}
});
}
/**
* Crawls all link elements (`<a href>`) on the current page, opens each link in a new tab,
* checks for console errors, and optionally verifies component tags.
*
* Logs detailed information, including errors and successes, to a timestamped log file.
*
* Ensures browser cleanup by closing new tabs and returning to the original one after each link.
*
* @param options - Optional configuration object.
* @param options.crawlForTags - If true, also runs `crawlForTags()` on each visited link. Defaults to `true`.
* @param options.customTags - Optional array of custom CSS selectors to pass to `crawlForTags()`.
* @param options.readySelector - CSS selector to wait for before checking console errors.
* Defaults to `'body'`. Use a more specific selector (e.g. `'#app > *'`) to ensure the
* page's JavaScript framework has finished rendering before logs are captured.
* @param options.ignorePatterns - Error log entries matching any of these strings or regexes
* will be silently ignored. Useful for known third-party errors that cannot be fixed.
* @param options.ignoreUrls - Page URLs matching any of these strings or regexes will be
* skipped entirely (all errors on that page are ignored). Useful for known-broken demos.
*
* @example
* ```typescript
* beforeAll(async () => {
* crawler = new Crawler();
* await crawler.navigateTo('http://localhost:4200/');
* });
*
* afterAll(async () => {
* await crawler.close();
* });
*
* it('crawl all links with URL-based tag detection', async () => {
* await crawler.crawlForErrors();
* });
*
* it('crawl all links with custom tags', async () => {
* await crawler.crawlForErrors({ crawlForTags: true, customTags: ['kendo-grid', 'kendo-button'] });
* });
*
* it('crawl all links without tag checking, wait for React to render, ignore known errors', async () => {
* await crawler.crawlForErrors({
* crawlForTags: false,
* readySelector: '#app > *',
* ignorePatterns: ['React Router', /third-party-lib/],
* });
* });
* ```
*/
crawlForErrors() {
return __awaiter(this, arguments, void 0, function* (options = {}) {
const { crawlForTags = true, customTags, readySelector = 'body', ignorePatterns = [], ignoreUrls = [], } = options;
// Create log file with timestamp
const now = new Date();
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
const logFileName = `crawl-log-${timestamp}.txt`;
const logFilePath = path_1.default.join(process.cwd(), logFileName);
// Helper function to log both to console and file
const logToFile = (message) => {
const timestampedMessage = `[${new Date().toISOString()}] ${message}\n`;
console.log(message);
fs_1.default.appendFileSync(logFilePath, timestampedMessage);
};
logToFile(`Starting crawl for errors at ${new Date().toISOString()}`);
logToFile(`Log file: ${logFileName}`);
logToFile(`Crawl for tags enabled: ${crawlForTags}`);
const linksVisible = yield this.isVisible(browser_1.By.css("a[href]"));
if (!linksVisible) {
throw new Error('Expected links (a[href]) to be visible on the page, but none were found.');
}
yield this.sleep(500);
const allLinksElements = yield this.findAll(browser_1.By.css("a[href]"));
logToFile(`Found ${allLinksElements.length} links to process.`);
let processedCount = 0;
let successCount = 0;
let errorCount = 0;
const errorUrls = [];
for (const linkElement of allLinksElements) {
processedCount++;
try {
// Get link href for logging
const linkHref = yield linkElement.getAttribute('href');
logToFile(`\n--- Processing link ${processedCount}/${allLinksElements.length}: ${linkHref} ---`);
// Get current window handles before clicking
const initialWindowHandles = yield this.driver.getAllWindowHandles();
const currentWindowHandle = yield this.driver.getWindowHandle();
yield this.click(linkElement);
// Check if a new tab/window was opened
const newWindowHandles = yield this.driver.getAllWindowHandles();
if (newWindowHandles.length > initialWindowHandles.length) {
// New tab was opened, switch to it
const newWindowHandle = newWindowHandles.find(handle => !initialWindowHandles.includes(handle));
if (newWindowHandle) {
yield this.driver.switchTo().window(newWindowHandle);
// Get the new tab's URL for logging
const newTabUrl = yield this.getCurrentUrl();
logToFile(`New tab opened: ${newTabUrl}`);
// Check if this URL should be skipped entirely
const isIgnoredUrl = ignoreUrls.some((pattern) => typeof pattern === 'string'
? newTabUrl.includes(pattern)
: pattern.test(newTabUrl));
if (isIgnoredUrl) {
logToFile(`SKIPPED (matches ignoreUrls pattern)`);
yield this.driver.close();
yield this.driver.switchTo().window(currentWindowHandle);
successCount++;
continue;
}
// Wait until the specified selector is present.
yield this.find(browser_1.By.css(readySelector));
yield this.sleep(10);
// Check for console errors in the new tab
const rawErrorLogs = yield this.getErrorLogs();
const errorLogs = rawErrorLogs.filter((err) => !ignorePatterns.some((pattern) => typeof pattern === 'string' ? err.includes(pattern) : pattern.test(err)));
const ignoredCount = rawErrorLogs.length - errorLogs.length;
if (ignoredCount > 0) {
logToFile(`Ignored ${ignoredCount} error(s) matching ignorePatterns.`);
}
if (errorLogs.length > 0) {
logToFile(`New console errors found (${errorLogs.length}):`);
errorLogs.forEach((error, index) => {
logToFile(` > Error ${index + 1}: ${error}`);
});
errorUrls.push({
url: newTabUrl,
errors: errorLogs,
});
}
else {
logToFile('No console errors found!');
}
if (errorLogs.length > 0) {
throw new Error(`Expected no console errors, but found ${errorLogs.length}: ${JSON.stringify(errorLogs)}`);
}
if (crawlForTags) {
// Optionally crawl for tags in the new tab based on the new tab's URL
logToFile('Crawling for tags...');
yield this.crawlForTags(customTags);
}
// Close the new tab and switch back to the original
yield this.driver.close();
yield this.driver.switchTo().window(currentWindowHandle);
logToFile(`Link ${processedCount} processed successfully`);
successCount++;
}
}
else {
const warningMsg = "No new window opened after clicking the link, continuing with next link.";
logToFile(`WARNING: ${warningMsg}`);
console.warn(warningMsg);
}
}
catch (error) {
errorCount++;
const errorMsg = `Error processing link ${processedCount}: ${error instanceof Error ? error.message : String(error)}`;
logToFile(`ERROR: ${errorMsg}`);
console.warn(errorMsg);
// Ensure we're back on the original tab even if something went wrong
try {
const currentHandles = yield this.driver.getAllWindowHandles();
// If we have multiple windows, close any extra ones and return to the first
if (currentHandles.length > 1) {
logToFile('Cleaning up extra windows...');
for (const handle of currentHandles) {
if (handle !== currentHandles[0]) {
yield this.driver.switchTo().window(handle);
yield this.driver.close();
}
}
yield this.driver.switchTo().window(currentHandles[0]);
logToFile('Cleanup completed, returned to original tab');
}
}
catch (cleanupError) {
const cleanupMsg = `Error during cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`;
logToFile(`CLEANUP ERROR: ${cleanupMsg}`);
console.warn(cleanupMsg);
}
// Continue with the next link to avoid failing the entire test
continue;
}
}
// Log final summary
logToFile(`\n=== CRAWL SUMMARY ===`);
logToFile(`Total links processed: ${processedCount}`);
logToFile(`Successful: ${successCount}`);
logToFile(`Errors: ${errorCount}`);
if (errorUrls.length > 0) {
logToFile(`\n=== URLS WITH ERRORS ===`);
errorUrls.forEach(({ url, errors }, idx) => {
logToFile(`${idx + 1}. ${url}`);
errors.forEach((err, errIdx) => {
logToFile(` [${errIdx + 1}] ${err}`);
});
});
}
logToFile(`Crawl completed at ${new Date().toISOString()}`);
logToFile(`Log saved to: ${logFilePath}`);
if (errorUrls.length > 0) {
throw new Error(`Crawl completed with ${errorUrls.length} URL(s) containing console errors. See log for details.`);
}
});
}
}
exports.Crawler = Crawler;
//# sourceMappingURL=crawler.js.map