e2eUtils.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. }
  49. export function setupPuppeteer(args?: string[]): PuppeteerUtils {
  50. let browser: Browser
  51. let page: Page
  52. const resolvedOptions = args
  53. ? {
  54. ...puppeteerOptions,
  55. args: [...puppeteerOptions.args!, ...args],
  56. }
  57. : puppeteerOptions
  58. beforeAll(async () => {
  59. browser = await puppeteer.launch(resolvedOptions)
  60. }, 20000)
  61. beforeEach(async () => {
  62. page = await browser.newPage()
  63. await page.evaluateOnNewDocument(() => {
  64. localStorage.clear()
  65. })
  66. page.on('console', e => {
  67. if (e.type() === 'error') {
  68. const err = e.args()[0]
  69. console.error(`Error from Puppeteer-loaded page:\n`, err.remoteObject())
  70. }
  71. })
  72. })
  73. afterEach(async () => {
  74. await page.close()
  75. })
  76. afterAll(async () => {
  77. await browser.close()
  78. })
  79. async function click(
  80. selector: string,
  81. options?: ClickOptions,
  82. ): Promise<void> {
  83. await page.click(selector, options)
  84. }
  85. async function count(selector: string): Promise<number> {
  86. return (await page.$$(selector)).length
  87. }
  88. async function text(selector: string): Promise<string | null> {
  89. return page.$eval(selector, node => node.textContent)
  90. }
  91. async function value(selector: string): Promise<string> {
  92. return page.$eval(selector, node => (node as HTMLInputElement).value)
  93. }
  94. async function html(selector: string): Promise<string> {
  95. return page.$eval(selector, node => node.innerHTML)
  96. }
  97. async function classList(selector: string): Promise<string[]> {
  98. return page.$eval(selector, (node: any) => [...node.classList])
  99. }
  100. async function children(selector: string): Promise<any[]> {
  101. return page.$eval(selector, (node: any) => [...node.children])
  102. }
  103. async function style(
  104. selector: string,
  105. property: keyof CSSStyleDeclaration,
  106. ): Promise<any> {
  107. return await page.$eval(
  108. selector,
  109. (node, property) => {
  110. return window.getComputedStyle(node)[property]
  111. },
  112. property,
  113. )
  114. }
  115. async function isVisible(selector: string): Promise<boolean> {
  116. const display = await page.$eval(selector, node => {
  117. return window.getComputedStyle(node).display
  118. })
  119. return display !== 'none'
  120. }
  121. async function isChecked(selector: string) {
  122. return await page.$eval(
  123. selector,
  124. node => (node as HTMLInputElement).checked,
  125. )
  126. }
  127. async function isFocused(selector: string) {
  128. return await page.$eval(selector, node => node === document.activeElement)
  129. }
  130. async function setValue(selector: string, value: string) {
  131. await page.$eval(
  132. selector,
  133. (node, value) => {
  134. ;(node as HTMLInputElement).value = value as string
  135. node.dispatchEvent(new Event('input'))
  136. },
  137. value,
  138. )
  139. }
  140. async function typeValue(selector: string, value: string) {
  141. const el = (await page.$(selector))!
  142. await el.evaluate(node => ((node as HTMLInputElement).value = ''))
  143. await el.type(value)
  144. }
  145. async function enterValue(selector: string, value: string) {
  146. const el = (await page.$(selector))!
  147. await el.evaluate(node => ((node as HTMLInputElement).value = ''))
  148. await el.type(value)
  149. await el.press('Enter')
  150. }
  151. async function clearValue(selector: string) {
  152. return await page.$eval(
  153. selector,
  154. node => ((node as HTMLInputElement).value = ''),
  155. )
  156. }
  157. function timeout(time: number) {
  158. return page.evaluate(time => {
  159. return new Promise(r => {
  160. setTimeout(r, time)
  161. })
  162. }, time)
  163. }
  164. function nextFrame() {
  165. return page.evaluate(() => {
  166. return new Promise(resolve => {
  167. requestAnimationFrame(() => {
  168. requestAnimationFrame(resolve)
  169. })
  170. })
  171. })
  172. }
  173. return {
  174. page: () => page,
  175. click,
  176. count,
  177. text,
  178. value,
  179. html,
  180. classList,
  181. style,
  182. children,
  183. isVisible,
  184. isChecked,
  185. isFocused,
  186. setValue,
  187. typeValue,
  188. enterValue,
  189. clearValue,
  190. timeout,
  191. nextFrame,
  192. }
  193. }