e2eUtils.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import puppeteer, {
  2. type Browser,
  3. type ClickOptions,
  4. type LaunchOptions,
  5. type Page,
  6. } from 'puppeteer'
  7. export const E2E_TIMEOUT: number = 30 * 1000
  8. const puppeteerOptions: LaunchOptions = {
  9. args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
  10. headless: true,
  11. }
  12. const maxTries = 30
  13. export const timeout = (n: number): Promise<any> =>
  14. new Promise(r => setTimeout(r, n))
  15. export async function expectByPolling(
  16. poll: () => Promise<any>,
  17. expected: string,
  18. ): Promise<void> {
  19. for (let tries = 0; tries < maxTries; tries++) {
  20. const actual = (await poll()) || ''
  21. if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
  22. expect(actual).toMatch(expected)
  23. break
  24. } else {
  25. await timeout(50)
  26. }
  27. }
  28. }
  29. interface PuppeteerUtils {
  30. page: () => Page
  31. click(selector: string, options?: ClickOptions): Promise<void>
  32. count(selector: string): Promise<number>
  33. text(selector: string): Promise<string | null>
  34. value(selector: string): Promise<string>
  35. html(selector: string): Promise<string>
  36. classList(selector: string): Promise<string[]>
  37. style(selector: string, property: keyof CSSStyleDeclaration): Promise<any>
  38. children(selector: string): Promise<any[]>
  39. isVisible(selector: string): Promise<boolean>
  40. isChecked(selector: string): Promise<boolean>
  41. isFocused(selector: string): Promise<boolean>
  42. setValue(selector: string, value: string): Promise<any>
  43. typeValue(selector: string, value: string): Promise<any>
  44. enterValue(selector: string, value: string): Promise<any>
  45. clearValue(selector: string): Promise<any>
  46. timeout(time: number): Promise<any>
  47. nextFrame(): Promise<any>
  48. transitionStart(
  49. btnSelector: string,
  50. containerSelector: string,
  51. ): Promise<{ classNames: string[]; innerHTML: string }>
  52. waitForElement(
  53. selector: string,
  54. text: string,
  55. classNames: string[],
  56. timeout?: number,
  57. ): Promise<any>
  58. }
  59. export function setupPuppeteer(args?: string[]): PuppeteerUtils {
  60. let browser: Browser
  61. let page: Page
  62. const resolvedOptions = args
  63. ? {
  64. ...puppeteerOptions,
  65. args: [...puppeteerOptions.args!, ...args],
  66. }
  67. : puppeteerOptions
  68. beforeAll(async () => {
  69. browser = await puppeteer.launch(resolvedOptions)
  70. }, 20000)
  71. beforeEach(async () => {
  72. page = await browser.newPage()
  73. await page.evaluateOnNewDocument(() => {
  74. localStorage.clear()
  75. })
  76. page.on('console', e => {
  77. if (e.type() === 'error') {
  78. console.error(`Error from Puppeteer-loaded page:\n`, e.text())
  79. }
  80. })
  81. })
  82. afterEach(async () => {
  83. await page.close()
  84. })
  85. afterAll(async () => {
  86. await browser.close()
  87. })
  88. async function click(
  89. selector: string,
  90. options?: ClickOptions,
  91. ): Promise<void> {
  92. await page.click(selector, options)
  93. }
  94. async function count(selector: string): Promise<number> {
  95. return (await page.$$(selector)).length
  96. }
  97. async function text(selector: string): Promise<string | null> {
  98. return page.$eval(selector, node => node.textContent)
  99. }
  100. async function value(selector: string): Promise<string> {
  101. return page.$eval(selector, node => (node as HTMLInputElement).value)
  102. }
  103. async function html(selector: string): Promise<string> {
  104. return page.$eval(selector, node => node.innerHTML)
  105. }
  106. async function classList(selector: string): Promise<string[]> {
  107. return page.$eval(selector, (node: any) => [...node.classList])
  108. }
  109. async function children(selector: string): Promise<any[]> {
  110. return page.$eval(selector, (node: any) => [...node.children])
  111. }
  112. async function style(
  113. selector: string,
  114. property: keyof CSSStyleDeclaration,
  115. ): Promise<any> {
  116. return await page.$eval(
  117. selector,
  118. (node, property) => {
  119. return window.getComputedStyle(node)[property]
  120. },
  121. property,
  122. )
  123. }
  124. async function isVisible(selector: string): Promise<boolean> {
  125. const display = await page.$eval(selector, node => {
  126. return window.getComputedStyle(node).display
  127. })
  128. return display !== 'none'
  129. }
  130. async function isChecked(selector: string) {
  131. return await page.$eval(
  132. selector,
  133. node => (node as HTMLInputElement).checked,
  134. )
  135. }
  136. async function isFocused(selector: string) {
  137. return await page.$eval(selector, node => node === document.activeElement)
  138. }
  139. async function setValue(selector: string, value: string) {
  140. await page.$eval(
  141. selector,
  142. (node, value) => {
  143. ;(node as HTMLInputElement).value = value as string
  144. node.dispatchEvent(new Event('input'))
  145. },
  146. value,
  147. )
  148. }
  149. async function typeValue(selector: string, value: string) {
  150. const el = (await page.$(selector))!
  151. await el.evaluate(node => ((node as HTMLInputElement).value = ''))
  152. await el.type(value)
  153. }
  154. async function enterValue(selector: string, value: string) {
  155. const el = (await page.$(selector))!
  156. await el.evaluate(node => ((node as HTMLInputElement).value = ''))
  157. await el.type(value)
  158. await el.press('Enter')
  159. }
  160. async function clearValue(selector: string) {
  161. return await page.$eval(
  162. selector,
  163. node => ((node as HTMLInputElement).value = ''),
  164. )
  165. }
  166. function timeout(time: number) {
  167. return page.evaluate(time => {
  168. return new Promise(r => {
  169. setTimeout(r, time)
  170. })
  171. }, time)
  172. }
  173. function nextFrame() {
  174. return page.evaluate(() => {
  175. return new Promise(resolve => {
  176. requestAnimationFrame(() => {
  177. requestAnimationFrame(resolve)
  178. })
  179. })
  180. })
  181. }
  182. const transitionStart = (btnSelector: string, containerSelector: string) =>
  183. page.evaluate(
  184. ([btnSel, containerSel]) => {
  185. ;(document.querySelector(btnSel) as HTMLElement)!.click()
  186. return Promise.resolve().then(() => {
  187. const container = document.querySelector(containerSel)!
  188. return {
  189. classNames: container.className.split(/\s+/g),
  190. innerHTML: container.innerHTML,
  191. }
  192. })
  193. },
  194. [btnSelector, containerSelector],
  195. )
  196. const waitForElement = (
  197. selector: string,
  198. text: string,
  199. classNames: string[], // if empty, check for no classes
  200. timeout = 2000,
  201. ) =>
  202. page.waitForFunction(
  203. (sel, expectedText, expectedClasses) => {
  204. const el = document.querySelector(sel)
  205. const hasClasses =
  206. expectedClasses.length === 0
  207. ? el?.classList.length === 0
  208. : expectedClasses.every(c => el?.classList.contains(c))
  209. const hasText = el?.textContent?.includes(expectedText)
  210. return !!el && hasClasses && hasText
  211. },
  212. { timeout },
  213. selector,
  214. text,
  215. classNames,
  216. )
  217. return {
  218. page: () => page,
  219. click,
  220. count,
  221. text,
  222. value,
  223. html,
  224. classList,
  225. style,
  226. children,
  227. isVisible,
  228. isChecked,
  229. isFocused,
  230. setValue,
  231. typeValue,
  232. enterValue,
  233. clearValue,
  234. timeout,
  235. nextFrame,
  236. transitionStart,
  237. waitForElement,
  238. }
  239. }