UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

287 lines 15.7 kB
"use strict"; 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