elements.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import { By } from 'selenium-webdriver'
  2. import fs from 'fs'
  3. import { expect } from 'chai'
  4. import { Condition } from 'selenium-webdriver'
  5. export const DEFAULT_UI_WAIT_MS = 3000
  6. const FAST_POLL_MS = 50
  7. // Keep Selenium helpers in lockstep with the frontend DOM id helpers.
  8. export {
  9. ARGUMENT_FIELD_ID_PREFIX,
  10. argumentFieldChoicesId,
  11. argumentFieldId,
  12. argumentFieldValidationElementId,
  13. argumentFieldValueId
  14. } from '../../frontend/resources/vue/utils/argumentFieldIds.js'
  15. const executionDialogStatusBy = By.css('.execution-dialog-status')
  16. const sidebarIds = ['mainnav', 'picocrank-sidebar']
  17. const sidebarNavLinksSelector = [
  18. sidebarCss('menu.navigation-links > li:not(.nav-section)'),
  19. sidebarCss('menu.nav-section-links > li')
  20. ].join(', ')
  21. let loadedPageGeneration = null
  22. function sidebarCss (suffix) {
  23. return sidebarIds.map((id) => `#${id} ${suffix}`).join(', ')
  24. }
  25. function isSamePagePath (currentUrl, targetUrl) {
  26. try {
  27. return new URL(currentUrl).pathname === new URL(targetUrl).pathname
  28. } catch {
  29. return false
  30. }
  31. }
  32. async function waitUntil (description, fn, timeoutMs = DEFAULT_UI_WAIT_MS) {
  33. await webdriver.wait(
  34. new Condition(description, fn),
  35. timeoutMs,
  36. undefined,
  37. FAST_POLL_MS
  38. )
  39. }
  40. async function getBodyAttribute (name) {
  41. return webdriver.executeScript(
  42. (attributeName) => document.body.getAttribute(attributeName),
  43. name
  44. )
  45. }
  46. async function isSidebarVisibleInBrowser () {
  47. return webdriver.executeScript((ids) => {
  48. for (const id of ids) {
  49. const sidebar = document.getElementById(id)
  50. if (!sidebar) {
  51. continue
  52. }
  53. const classes = sidebar.className
  54. if (classes.includes('shown') || classes.includes('stuck')) {
  55. return true
  56. }
  57. }
  58. return false
  59. }, sidebarIds)
  60. }
  61. async function countNavigationLinksInBrowser () {
  62. return webdriver.executeScript(
  63. (selector) => document.querySelectorAll(selector).length,
  64. sidebarNavLinksSelector
  65. )
  66. }
  67. export async function getActionButtons () {
  68. // Currently, only the active dashboard's contents are rendered,
  69. // so we don't need to scope the selector by dashboard title.
  70. return await webdriver.findElements(By.css('.action-button button'))
  71. }
  72. export async function getExecutionDialogOutput() {
  73. await waitUntil('Dialog with long int is visible', async () => {
  74. const dialog = await webdriver.findElement({ id: 'execution-results-popup' })
  75. return await dialog.isDisplayed()
  76. })
  77. const ret = await webdriver.executeScript('return window.logEntries.get(window.executionDialog.executionTrackingId).output')
  78. return ret
  79. }
  80. export async function closeExecutionDialog() {
  81. const btnClose = await webdriver.findElements(By.css('[title="Close"]'))
  82. await btnClose[0].click()
  83. }
  84. export function takeScreenshotOnFailure (test, webdriver) {
  85. if (test.state === 'failed') {
  86. const title = test.fullTitle();
  87. console.log(`Test failed, taking screenshot: ${title}`);
  88. takeScreenshot(webdriver, title);
  89. }
  90. }
  91. export function takeScreenshot (webdriver, title) {
  92. return webdriver.takeScreenshot().then((img) => {
  93. fs.mkdirSync('screenshots', { recursive: true });
  94. title = title.replaceAll('config: ', '')
  95. title = title.replaceAll(/[()|*<>:]/g, '_')
  96. title = title + '.failed-test'
  97. fs.writeFileSync('screenshots/' + title + '.png', img, 'base64')
  98. })
  99. }
  100. export async function waitForDashboardLoaded(timeoutMs = DEFAULT_UI_WAIT_MS, expectedTitle = null) {
  101. await waitUntil('wait for loaded-dashboard', async function () {
  102. const attr = await getBodyAttribute('loaded-dashboard')
  103. if (attr == null || attr === '') {
  104. return false
  105. }
  106. if (expectedTitle != null) {
  107. return attr === expectedTitle
  108. }
  109. return true
  110. }, timeoutMs)
  111. }
  112. export async function waitForLogsPage(timeoutMs = DEFAULT_UI_WAIT_MS) {
  113. await waitUntil('wait for logs page', async () => {
  114. const url = await webdriver.getCurrentUrl()
  115. return url.includes('/logs/') && !url.endsWith('/logs')
  116. }, timeoutMs)
  117. }
  118. export async function waitForArgumentFormPage(timeoutMs = DEFAULT_UI_WAIT_MS) {
  119. await waitUntil('wait for argument form page', async () => {
  120. const url = await webdriver.getCurrentUrl()
  121. return url.includes('/actionBinding/') && url.includes('/argumentForm')
  122. }, timeoutMs)
  123. }
  124. export async function waitForArgumentFormReady(timeoutMs = DEFAULT_UI_WAIT_MS) {
  125. await waitUntil('wait for argument form ready', async () => {
  126. const attr = await getBodyAttribute('loaded-argument-form')
  127. return attr != null && attr !== ''
  128. }, timeoutMs)
  129. }
  130. export async function waitForExecutionComplete(timeoutMs = DEFAULT_UI_WAIT_MS) {
  131. await waitUntil('wait for execution to finish', async () => {
  132. const statusElements = await webdriver.findElements(executionDialogStatusBy)
  133. if (statusElements.length === 0) {
  134. return false
  135. }
  136. try {
  137. const statusText = await statusElements[0].getText()
  138. return !statusText.includes('Still running') && !statusText.includes('Queued')
  139. } catch {
  140. return false
  141. }
  142. }, timeoutMs)
  143. }
  144. export async function getRootAndWait() {
  145. const targetUrl = runner.baseUrl()
  146. const sameConfig = loadedPageGeneration === runner.pageGeneration
  147. if (sameConfig && isSamePagePath(await webdriver.getCurrentUrl(), targetUrl)) {
  148. const attr = await getBodyAttribute('loaded-dashboard')
  149. if (attr != null && attr !== '') {
  150. return
  151. }
  152. }
  153. await webdriver.get(targetUrl)
  154. await waitForDashboardLoaded()
  155. loadedPageGeneration = runner.pageGeneration
  156. }
  157. async function isSidebarVisible () {
  158. return isSidebarVisibleInBrowser()
  159. }
  160. export async function closeSidebar() {
  161. if (await isSidebarVisible()) {
  162. await webdriver.findElement(By.id('sidebar-toggler-button')).click()
  163. }
  164. await waitUntil('wait for sidebar to close', async () => {
  165. return !(await isSidebarVisible())
  166. })
  167. }
  168. export async function openSidebar() {
  169. if (await isSidebarVisible()) {
  170. return
  171. }
  172. await webdriver.findElement(By.id('sidebar-toggler-button')).click()
  173. await waitUntil('wait for sidebar to open', async () => {
  174. return await isSidebarVisible()
  175. })
  176. }
  177. export async function getNavigationLinks() {
  178. return await webdriver.findElements(By.css(sidebarNavLinksSelector))
  179. }
  180. export async function getNavigationLinkTitles () {
  181. return webdriver.executeScript((selector) => {
  182. return [...document.querySelectorAll(selector)].map((linkElement) => {
  183. const title = linkElement.getAttribute('title')
  184. if (title) {
  185. return title
  186. }
  187. const anchor = linkElement.querySelector('a[href]')
  188. return anchor ? anchor.textContent.trim() : ''
  189. })
  190. }, sidebarNavLinksSelector)
  191. }
  192. export async function waitForNavigationLinks (minimumCount = 1, timeoutMs = DEFAULT_UI_WAIT_MS) {
  193. await waitUntil(`wait for at least ${minimumCount} navigation links`, async () => {
  194. const count = await countNavigationLinksInBrowser()
  195. return count >= minimumCount
  196. }, timeoutMs)
  197. }
  198. export async function getNavigationLinkTitle (linkElement) {
  199. const title = await linkElement.getAttribute('title')
  200. if (title) {
  201. return title
  202. }
  203. const anchor = await linkElement.findElement(By.css('a[href]'))
  204. return await anchor.getText()
  205. }
  206. export async function findSidebarNavHref (href) {
  207. return await webdriver.findElements(By.css(sidebarCss(`a[href="${href}"]`)))
  208. }
  209. export async function requireExecutionDialogStatus (webdriver, expected) {
  210. await waitUntil('wait for action to be running', async function () {
  211. const dialogStatus = await webdriver.findElement(executionDialogStatusBy)
  212. const actual = await dialogStatus.getText()
  213. return actual === expected
  214. })
  215. }
  216. export async function findExecutionDialog (webdriver) {
  217. return webdriver.findElement(By.id('execution-results-popup'))
  218. }
  219. export async function getActionButton (webdriver, title) {
  220. const buttons = await webdriver.findElements(By.css('[title="' + title + '"]'))
  221. expect(buttons).to.have.length(1)
  222. return buttons[0]
  223. }
  224. export async function getTerminalBuffer() {
  225. try {
  226. const output = await webdriver.executeScript(`
  227. if (window.terminal && window.terminal.getBufferAsString) {
  228. return window.terminal.getBufferAsString();
  229. }
  230. return null;
  231. `)
  232. return output
  233. } catch (e) {
  234. console.log('[getTerminalBuffer] Error:', e.message)
  235. return null
  236. }
  237. }
  238. export async function waitForCurrentUrl (predicate, timeoutMs = DEFAULT_UI_WAIT_MS) {
  239. await waitUntil('wait for url', async () => {
  240. const url = await webdriver.getCurrentUrl()
  241. return predicate(url)
  242. }, timeoutMs)
  243. }
  244. export async function waitForSelectorCount (selector, minimumCount = 1, timeoutMs = DEFAULT_UI_WAIT_MS) {
  245. await waitUntil(`wait for at least ${minimumCount} ${selector}`, async () => {
  246. const count = await webdriver.executeScript(
  247. (cssSelector) => document.querySelectorAll(cssSelector).length,
  248. selector
  249. )
  250. return count >= minimumCount
  251. }, timeoutMs)
  252. }