PlaywrightAddon.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. const Helper = require("@codeceptjs/helper");
  2. const ElementNotFound = require("codeceptjs/lib/helper/errors/ElementNotFound");
  3. const assert = require("assert");
  4. function assertElementExists(res, locator, prefix, suffix) {
  5. if (!res || res.length === 0) {
  6. throw new ElementNotFound(locator, prefix, suffix);
  7. }
  8. }
  9. class PlaywrightAddon extends Helper {
  10. /**
  11. * Grab CSS property for given locator with pseudo element
  12. * Resumes test execution, so **should be used inside an async function with `await`** operator.
  13. * If more than one element is found - value of first element is returned.
  14. *
  15. * ```js
  16. * const value = await I.grabCssPropertyFromPseudo('h3', 'font-weight', 'after');
  17. * ```
  18. *
  19. * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
  20. * @param {string} cssProperty CSS property name.
  21. * @param {string} pseudoElement Pseudo element name.
  22. * @returns {Promise<string>} CSS value
  23. */
  24. async grabCssPropertyFromPseudo(locator, cssProperty, pseudoElement) {
  25. const cssValues = await this.grabCssPropertyFromAllPseudo(locator, cssProperty, pseudoElement);
  26. assertElementExists(cssValues, locator);
  27. this.helpers.Playwright.debugSection("CSS", cssValues[0]);
  28. return cssValues[0];
  29. }
  30. /**
  31. * Grab array of CSS properties for given locator with pseudo element
  32. * Resumes test execution, so **should be used inside an async function with `await`** operator.
  33. *
  34. * ```js
  35. * const values = await I.grabCssPropertyFromAllPseudo('h3', 'font-weight', 'after');
  36. * ```
  37. *
  38. * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
  39. * @param {string} cssProperty CSS property name.
  40. * @param {string} pseudoElement Pseudo element name.
  41. * @returns {Promise<string[]>} CSS value
  42. */
  43. async grabCssPropertyFromAllPseudo(locator, cssProperty, pseudoElement) {
  44. const els = await this.helpers.Playwright._locate(locator);
  45. this.helpers.Playwright.debug(`Matched ${els.length} elements`);
  46. return await Promise.all(
  47. els.map((el) =>
  48. el.evaluate(
  49. (el, { cssProperty, pseudoElement }) => getComputedStyle(el, pseudoElement).getPropertyValue(cssProperty),
  50. { cssProperty, pseudoElement },
  51. ),
  52. ),
  53. );
  54. }
  55. async seeFocusedElement(selector, { timeout = 2000 } = {}) {
  56. const startTime = Date.now();
  57. const checkInterval = 16;
  58. let isFocused = false;
  59. let lastError;
  60. while (Date.now() - startTime < timeout) {
  61. try {
  62. const els = await this.helpers.Playwright._locate(selector);
  63. const areFocused = await Promise.all(els.map((el) => el.evaluate((el) => el === document.activeElement)));
  64. if (areFocused.some((el) => el)) {
  65. isFocused = true;
  66. break;
  67. }
  68. lastError = null;
  69. } catch (error) {
  70. lastError = error;
  71. }
  72. await this.helpers.Playwright.page.waitForTimeout(checkInterval);
  73. }
  74. assert.ok(isFocused, `Element ${selector} is not focused after ${timeout}ms${lastError ? `:\n${lastError}` : ""}`);
  75. }
  76. /**
  77. * Get or create CDP client for performance operations
  78. */
  79. async _getCDPClient() {
  80. try {
  81. const { page } = this.helpers.Playwright;
  82. // Check if page is still valid
  83. if (!page || page.isClosed()) {
  84. this._cdpClient = null;
  85. throw new Error("Page is closed or invalid");
  86. }
  87. // Create new session
  88. this._cdpClient = await page.context().newCDPSession(page);
  89. return this._cdpClient;
  90. } catch (error) {
  91. this._cdpClient = null;
  92. throw new Error(`Failed to create CDP session: ${error.message}`);
  93. }
  94. }
  95. /**
  96. * Clean up CDP client
  97. */
  98. async _cleanupCDPClient() {
  99. if (this._cdpClient) {
  100. try {
  101. await this._cdpClient.detach();
  102. } catch (error) {
  103. // Ignore cleanup errors
  104. } finally {
  105. this._cdpClient = null;
  106. }
  107. }
  108. }
  109. /**
  110. * Throttle CPU performance using Chrome DevTools Protocol
  111. * @param {number} rate - CPU throttling rate (1 = normal, 2 = 2x slower, 4 = 4x slower, etc.)
  112. */
  113. async throttleCPU(rate = 1) {
  114. if (rate < 1) {
  115. throw new Error("CPU throttling rate must be >= 1");
  116. }
  117. try {
  118. const client = await this._getCDPClient();
  119. await client.send("Emulation.setCPUThrottlingRate", { rate });
  120. this._CPUThrottlingRate = rate;
  121. this.helpers.Playwright.debugSection("CPU", `Throttling set to ${rate}x slower`);
  122. return this;
  123. } catch (error) {
  124. this.helpers.Playwright.debugSection("CPU", `Failed to throttle: ${error.message}`);
  125. // Clean up broken client
  126. await this._cleanupCDPClient();
  127. throw error;
  128. }
  129. }
  130. /**
  131. * Reset CPU to normal performance
  132. */
  133. async resetCPU() {
  134. try {
  135. return await this.throttleCPU(1);
  136. } catch (error) {
  137. // Ignore errors when page is closed - CPU will be reset automatically
  138. return this;
  139. }
  140. }
  141. /**
  142. * CodeceptJS hook - cleanup CDP client after each test
  143. */
  144. async _after() {
  145. if (!this._CPUThrottlingRate || this._CPUThrottlingRate === 1) return;
  146. // Try to reset CPU before cleanup, but don't fail if page is closed
  147. try {
  148. await this.resetCPU();
  149. } catch (error) {
  150. // Ignore - page might be closed already
  151. }
  152. return this._cleanupCDPClient();
  153. }
  154. /**
  155. * CodeceptJS hook - cleanup CDP client after all tests are finished
  156. */
  157. _finishTest() {
  158. return this._cleanupCDPClient();
  159. }
  160. }
  161. module.exports = PlaywrightAddon;