github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/design/accepted/ui-testing.md (about) 1 # Proposal: How to test the lakeFS UI 2 3 Currently, there are no automated tests for the lakeFS UI. The lack of automated tests decreases the confidence to 4 introduce UI changes, reduces development velocity, and leads to unnoticed bugs of a changing severity, that 5 can sometimes even effect main UI workflows. 6 7 Given that we currently have zero UI testing and with cost-effectivity in mind, this proposal suggests an incremental 8 approach that will take us from zero to confidence in the main UI workflows, and does not aim to introduce a 100% 9 coverage solution. 10 11 ### Goals 12 13 * Increase the confidence that main UI workflows do not break. 14 * Minimize the cost of writing tests. 15 * Define a testing scope that is achievable at a reasonable timeframe. 16 * The tests are used both locally and as part of CI. 17 * Use JavaScript to test code written in JavaScript. 18 19 ### Non-goals 20 21 * Unit-test the [webui package](../webui) 22 * Test the UI visualization 23 * Stress-test the UI 24 25 ## Proposal 26 27 Implement E2E testing for a set of main UI workflows against a real lakeFS server. Use [Jest](https://jestjs.io/docs/tutorial-react) 28 which is the [most recommended](https://reactjs.org/docs/testing.html#tools) testing framework for React apps together 29 with a testing automation framework of our choice ([Puppeteer](https://github.com/puppeteer/puppeteer) or 30 [Selenium](https://www.selenium.dev/documentation/)) that allow interacting with a browser for testing purposes. 31 32 Below is a comparison of the two testing automation frameworks based on research and experiment followed by code examples. 33 34 ### Puppeteer Vs. Selenium 35 36 | | **Puppeteer** | **Selenium** | 37 | :---: | :---: | :---: | 38 | **Development experience** | Good experience, it was easy to understand what to search for, and I found the code readable. | Unpleasant experience. due to the lack of documentation, and some basic functionality that's not working for the JavaScript-Selenium combination. I had to find [workarounds](https://stackoverflow.com/questions/25583641/set-value-of-input-instead-of-sendkeys-selenium-webdriver-nodejs) to trigger simple operations. Also, it took double the time to write the same tests with Selenium and it was the second framework I experimented with (I gained some experience working with Puppeteer)| 39 | **Available docs (Jest + #)** | It is easy to find online resources because Puppeteer and Jest are Node libraries | It was hard to find references for Selenium in JavaScript, most docs include Java or Python examples.| 40 | **Debugging options** | Can slow down the test and view a browser running the test, can use a debugger | Can view a browser running the test and use a debugger | 41 | **Setup experience** | Easy to install and configure | Easy to install, does not require additional configurations | 42 | **Supported browsers** | Chrome | Supports number of browsers | 43 44 ### Example Code 45 46 The code below demonstrates how to use each testing automation framework to implement tests for the lakeFS Login workflow. 47 The tests assume a running local lakeFS server. 48 49 #### Jest & Puppeteer 50 51 ```javascript 52 //login.test.js 53 const timeout = process.env.SLOWMO ? 30000 : 10000; 54 55 describe('executing tests on login page with Puppeteer', () => { 56 beforeEach(async () => { 57 await page.goto(`${URL}/auth/login`, {waitUntil: 'domcontentloaded'}); 58 }); 59 60 test('Submit login form with invalid data', async () => { 61 await page.waitForSelector('.login-widget'); 62 await page.type('#username', 'INVALIDEXAMPLE'); 63 await page.type('#password','INVALIDEXAMPLE'); 64 65 await page.click('[type="submit"]'); 66 67 await page.waitForSelector('.login-error'); 68 const err = await page.$('.login-error'); // example of selecting HTML elements by CSS selector 69 const html = await page.evaluate(err => err.innerHTML, err); 70 expect(html).toBe("invalid credentials"); 71 }, timeout); 72 73 test('Submit login form with valid data', async () => { 74 await page.waitForSelector('.login-widget'); 75 await page.type('#username', 'AKIAIOSFODNN7EXAMPLE'); 76 await page.type('#password','wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'); 77 78 await Promise.all([ 79 await page.click('[type="submit"]'), 80 page.waitForNavigation(), 81 ]); 82 83 expect(page.url()).toBe("http://localhost:3000/repositories"); 84 }, timeout); 85 }) 86 ``` 87 88 #### Jest & Selenium 89 90 ```javascript 91 //login.test.js 92 const webdriver = require("selenium-webdriver"); 93 const {until} = require("selenium-webdriver"); 94 const LakefsUrl = "http://localhost:8000" 95 96 describe('executing tests on login page with Selenium', () => { 97 98 let driver; 99 driver = new webdriver.Builder().forBrowser('chrome').build(); 100 101 beforeEach(async () => { 102 await driver.get(LakefsUrl+'/auth/login',{waitUntil: 'domcontentloaded'}); 103 }) 104 105 afterAll(async () => { 106 await driver.quit(); 107 }) 108 109 test('Submit login form with invalid data', async () => { 110 const login = await driver.findElement({className:"login-btn"}); 111 const username = await driver.findElement({id:"username"}); 112 await driver.executeScript("arguments[0].setAttribute('value', 'INVALIDEXAMPLE')", username); // https://stackoverflow.com/questions/25583641/set-value-of-input-instead-of-sendkeys-selenium-webdriver-nodejs 113 const password = await driver.findElement({id:"password"}); 114 await driver.executeScript("arguments[0].setAttribute('value', 'INVALIDEXAMPLE')", password); 115 116 await login.click(); 117 const until = webdriver.until; 118 var err = driver.wait(until.elementLocated({className:'login-error'}), 5000); 119 const errTxt = await err.getAttribute("innerHTML"); 120 expect(errTxt).toBe("invalid credentials"); 121 }, 10000); 122 123 test('Submit login form with valid data', async () => { 124 const login = await driver.findElement({className:"login-btn"}); 125 const username = await driver.findElement({id:"username"}); 126 await driver.executeScript("arguments[0].setAttribute('value', 'AKIAIOSFODNN7EXAMPLE')", username); 127 const password = await driver.findElement({id:"password"}); 128 await driver.executeScript("arguments[0].setAttribute('value', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY')", password); 129 130 await Promise.all([ 131 await login.click(), 132 driver.wait(until.urlIs("http://localhost:3000/repositories"), 5000) // I didn't find a stright forward way to wait for navigation 133 ]); 134 const actual = await driver.getCurrentUrl(); 135 expect(actual).toBe("http://localhost:3000/repositories"); 136 }, 10000); 137 }) 138 ``` 139 140 ### Recommendation 141 142 The main arguments that turn my recommendation towards **Puppeteer** as the testing automation framework are: 143 * The lakeFS UI is written in React, and using a framework that lives at the same space is more natural and convenient, 144 as opposed to Selenium which is used for multiple types of applications. 145 * The development experience working with Puppeteer was much better. 146 * Online resources testify that Puppeteer executes faster than Selenium. 147 148 ### Why test against a live server? 149 150 Writing and maintaining tests that rely on a live server is faster than using mocks. 151 Also, by using a live server changes to the server code are automatically tested from the UI side. 152 153 ### How to make the tests runnable locally and part of the CI process 154 155 To allow running the tests locally and as part of CI, we need to spin up a lakeFS and postgres instances 156 the tests can communicate with. We can use [Testcontainers](https://www.npmjs.com/package/testcontainers) to 157 spin the instances up as docker containers. 158 159 Then, to run the tests locally we can use 160 ```shell 161 npm test 162 ``` 163 164 As for CI, the [node workflow](../.github/workflows/node.yaml) is already running the webui tests that currently does 165 not exist. after imlementing the tests this workflow will be responsible for running them. 166 167 _**Notes:**_ 168 1. The decision on the testing automation framework will not affect this section. 169 2. Its TBD to decide exactly how to work with Testcontainers to do the tests setup; what the containers 170 should include, and the frequency of creating and tearing them down (In each test file, single instance for the whole 171 test suite etc.) 172 173 ### Future improvements 174 175 In the proposed increment, Jest is used as the E2E tests runner. As a second step we can use Jest in combination with 176 other React testing libraries to to unit-test the UI. As a third increment, when the UI becomes more stable we can use more advanced features provided by the testing automation framework such as screenshot 177 testing, performance testing, etc. 178 179 ### Decisions 180 181 * Use Jest and Puppeteer. 182 * Use [Testcontainers](https://www.npmjs.com/package/testcontainers) to spin up containerized lakeFS test instances. 183 184 ### References 185 * https://medium.com/touch4it/end-to-end-testing-with-puppeteer-and-jest-ec8198145321 186 * https://www.browserstack.com/guide/puppeteer-vs-selenium 187 * https://www.browserstack.com/guide/automation-using-selenium-javascript 188 * https://jestjs.io/docs/puppeteer 189 * https://github.com/puppeteer/puppeteer 190 * https://levelup.gitconnected.com/running-puppeteer-with-jest-on-github-actions-for-automated-testing-with-coverage-6cd15bc843b0 191 * https://blog.testproject.io/2020/02/20/selenium-vs-puppeteer-when-to-choose-what/ 192 * https://www.blazemeter.com/blog/selenium-vs-puppeteer-for-test-automation