justification.mjs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { describe, it, before, after } from 'mocha'
  2. import { expect } from 'chai'
  3. import { By, Condition } from 'selenium-webdriver'
  4. import {
  5. DEFAULT_UI_WAIT_MS,
  6. argumentFieldId,
  7. getRootAndWait,
  8. getActionButton,
  9. takeScreenshotOnFailure,
  10. waitForArgumentFormPage,
  11. waitForArgumentFormReady,
  12. waitForLogsPage,
  13. waitForExecutionComplete,
  14. getTerminalBuffer,
  15. } from '../../lib/elements.js'
  16. async function openArgumentForm (actionTitle) {
  17. await getRootAndWait()
  18. const btn = await getActionButton(webdriver, actionTitle)
  19. await btn.click()
  20. await waitForArgumentFormPage()
  21. await waitForArgumentFormReady()
  22. }
  23. async function getStartButton () {
  24. return await webdriver.findElement(By.css('button[name="start"]'))
  25. }
  26. async function getJustificationInput () {
  27. return await webdriver.findElement(By.id('justification'))
  28. }
  29. async function getJustificationValue () {
  30. const input = await getJustificationInput()
  31. return await input.getAttribute('value')
  32. }
  33. async function waitForJustificationValue (expected) {
  34. await webdriver.wait(
  35. new Condition(`wait for justification value "${expected}"`, async () => {
  36. const value = await getJustificationValue()
  37. return value === expected
  38. }),
  39. DEFAULT_UI_WAIT_MS
  40. )
  41. }
  42. async function fillJustification (text) {
  43. const input = await getJustificationInput()
  44. await input.clear()
  45. await input.sendKeys(text)
  46. await webdriver.sleep(100)
  47. }
  48. async function fillArgumentField (argumentName, value) {
  49. const input = await webdriver.findElement(By.id(argumentFieldId(argumentName)))
  50. await webdriver.executeScript(
  51. `const input = arguments[0];
  52. input.value = arguments[1];
  53. input.dispatchEvent(new Event('input', { bubbles: true }));
  54. input.dispatchEvent(new Event('change', { bubbles: true }));`,
  55. input,
  56. value
  57. )
  58. await webdriver.sleep(100)
  59. }
  60. async function submitForm () {
  61. const submitButton = await getStartButton()
  62. await submitButton.click()
  63. }
  64. async function assertStillOnArgumentForm () {
  65. const url = await webdriver.getCurrentUrl()
  66. expect(url).to.include('/argumentForm')
  67. }
  68. async function waitForTerminalOutput (expectedSubstring) {
  69. await webdriver.wait(
  70. new Condition(`wait for terminal output containing ${expectedSubstring}`, async () => {
  71. try {
  72. const terminalReady = await webdriver.executeScript(`
  73. return !!(window.terminal && window.terminal.getBufferAsString);
  74. `)
  75. if (!terminalReady) {
  76. return false
  77. }
  78. const output = await getTerminalBuffer()
  79. return output && output.includes(expectedSubstring)
  80. } catch (e) {
  81. return false
  82. }
  83. }),
  84. DEFAULT_UI_WAIT_MS
  85. )
  86. }
  87. async function waitForExecutionJustification (expectedText) {
  88. await webdriver.wait(
  89. new Condition(`wait for execution justification "${expectedText}"`, async () => {
  90. const elements = await webdriver.findElements(By.css('.log-justification'))
  91. if (elements.length === 0) {
  92. return false
  93. }
  94. const text = await elements[0].getText()
  95. return text.includes(expectedText)
  96. }),
  97. DEFAULT_UI_WAIT_MS
  98. )
  99. }
  100. async function submitWithJustification (text) {
  101. await fillJustification(text)
  102. await submitForm()
  103. await waitForLogsPage()
  104. await waitForExecutionComplete()
  105. }
  106. describe('config: justification', function () {
  107. this.timeout(10000)
  108. before(async function () {
  109. await runner.start('justification')
  110. })
  111. after(async () => {
  112. await runner.stop()
  113. })
  114. afterEach(function () {
  115. takeScreenshotOnFailure(this.currentTest, webdriver)
  116. })
  117. it('Actions without justification skip the argument form', async function () {
  118. await getRootAndWait()
  119. const btn = await getActionButton(webdriver, 'Test action without justification')
  120. await btn.click()
  121. await webdriver.wait(
  122. new Condition('navigates away from argument form', async () => {
  123. const url = await webdriver.getCurrentUrl()
  124. return url.includes('/logs/') && !url.includes('/argumentForm')
  125. }),
  126. DEFAULT_UI_WAIT_MS
  127. )
  128. await waitForExecutionComplete()
  129. await waitForTerminalOutput('No justification needed')
  130. })
  131. it('Manual justification opens a required empty field with no arguments', async function () {
  132. await openArgumentForm('Test manual justification')
  133. const justificationInput = await getJustificationInput()
  134. expect(await justificationInput.getTagName()).to.equal('input')
  135. expect(await justificationInput.getAttribute('type')).to.equal('text')
  136. expect(await justificationInput.getAttribute('required')).to.equal('true')
  137. expect((await getJustificationValue()).trim()).to.equal('')
  138. const label = await webdriver.findElement(By.css('label[for="justification"]'))
  139. expect(await label.getText()).to.contain('Justification')
  140. const argumentFields = await webdriver.findElements(By.css('[id^="arg-field-"]'))
  141. expect(argumentFields).to.have.length(0)
  142. })
  143. it('Manual justification blocks submit until filled', async function () {
  144. await openArgumentForm('Test manual justification')
  145. await submitForm()
  146. await assertStillOnArgumentForm()
  147. const justificationInput = await getJustificationInput()
  148. const validationMessage = await webdriver.executeScript(
  149. 'return arguments[0].validationMessage',
  150. justificationInput
  151. )
  152. expect(validationMessage).to.not.equal('')
  153. })
  154. it('Manual justification is stored and shown on the execution page', async function () {
  155. await openArgumentForm('Test manual justification')
  156. await submitWithJustification('Approved maintenance window')
  157. await waitForTerminalOutput('Manual justification action ran')
  158. await waitForExecutionJustification('Approved maintenance window')
  159. })
  160. it('Templated justification prefills from argument defaults', async function () {
  161. await openArgumentForm('Test templated justification')
  162. const targetInput = await webdriver.findElement(By.id(argumentFieldId('target')))
  163. expect(await targetInput.getAttribute('value')).to.equal('dbserver')
  164. await waitForJustificationValue('dbserver')
  165. await submitForm()
  166. await waitForLogsPage()
  167. await waitForExecutionComplete()
  168. await waitForTerminalOutput('Target: dbserver')
  169. await waitForExecutionJustification('dbserver')
  170. })
  171. it('Templated justification updates when argument values change', async function () {
  172. await openArgumentForm('Test templated justification')
  173. await waitForJustificationValue('dbserver')
  174. await fillArgumentField('target', 'appserver')
  175. await waitForJustificationValue('appserver')
  176. })
  177. it('Manual edits to templated justification are preserved when arguments change', async function () {
  178. await openArgumentForm('Test templated justification')
  179. await fillArgumentField('target', 'hosta')
  180. await waitForJustificationValue('hosta')
  181. await fillJustification('Manual override reason')
  182. expect(await getJustificationValue()).to.equal('Manual override reason')
  183. await fillArgumentField('target', 'hostb')
  184. await webdriver.sleep(200)
  185. expect(await getJustificationValue()).to.equal('Manual override reason')
  186. })
  187. it('Justification is required alongside regular arguments', async function () {
  188. await openArgumentForm('Test justification with arguments')
  189. const targetInput = await webdriver.findElement(By.id(argumentFieldId('target')))
  190. expect(await targetInput.getAttribute('value')).to.equal('testhost')
  191. await submitForm()
  192. await assertStillOnArgumentForm()
  193. await submitWithJustification('Deploy to testhost')
  194. await waitForTerminalOutput('Target: testhost')
  195. await waitForExecutionJustification('Deploy to testhost')
  196. })
  197. })