e2eUtils.ts 7.5 KB

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