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