Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI unit testing setup (KANBAN-601) #281

Merged
merged 29 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9c625ca
First attempt at implementing jest unit testing framework
mluypaert Aug 21, 2024
c26dea9
Makefile cleanup
mluypaert Aug 21, 2024
16340f7
Merge branch 'main' into KANBAN-601_ui-unit-testing-setup
mluypaert Sep 2, 2024
b1fdd00
Silenced console output
mluypaert Sep 2, 2024
d0baabb
Code cleanup
mluypaert Sep 2, 2024
d0f135e
Fixed jest type import file pattern
mluypaert Sep 2, 2024
fbb7016
Added webUI unit testing to PR validation
mluypaert Sep 2, 2024
1e715a7
Test AlignmentEntry component elements
mluypaert Sep 2, 2024
45b561e
Added cypress e2e testing examples
mluypaert Sep 3, 2024
d117161
use auto-updated webUI dependency lock file in GHA E2E testing
mluypaert Sep 3, 2024
fcab5de
Use webui-compatible node.js version in GHA E2E testing
mluypaert Sep 3, 2024
2ec1f78
Fixed jest typescript typing
mluypaert Sep 3, 2024
af4f7f2
Fixed jest/globals eslint configuration
mluypaert Sep 3, 2024
0fdfe64
Updated webui deps lock file
mluypaert Sep 3, 2024
9c16d94
Sorted devDependencies list
mluypaert Sep 3, 2024
63ca88d
Added default empty submit form behaviour testing
mluypaert Sep 4, 2024
ba12fa5
Cypress boilerplate cleanup
mluypaert Sep 4, 2024
c763b34
Added webUI startup and stop to E2E testing target
mluypaert Sep 4, 2024
956c6a9
Exclude cypress files from TS compilation (cypress handles TS compila…
mluypaert Sep 4, 2024
819b49e
Added dev webUI startup to GHA cypress run
mluypaert Sep 4, 2024
d1e57a3
Defined default baseUrl in cypress config
mluypaert Sep 4, 2024
9dc0693
Cypress config cleanup
mluypaert Sep 4, 2024
1edf04c
Added job-submit workflow E2E testing
mluypaert Sep 4, 2024
8420e35
Cypress boilerplate cleanup
mluypaert Sep 4, 2024
51805fe
Enable cypress dev testing on any environment (local containers or de…
mluypaert Sep 4, 2024
36d837c
Run GHA E2E testing on API and webUI container images
mluypaert Sep 4, 2024
ffc135b
Fix code checkout timing for GHA E2E testing
mluypaert Sep 4, 2024
35083ec
corrected API port for E2E testing webUI container execution
mluypaert Sep 4, 2024
fe2992f
Corrected API health check endpoint for E2E testing
mluypaert Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/PR-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,8 @@ jobs:
- name: Code style test
run: |
make run-style-checks
- name: Unit tests
run: make run-unit-tests
webui-container-image-build:
name: webUI container-image build
needs:
Expand Down Expand Up @@ -945,6 +947,67 @@ jobs:
push: false
tags: agr_pavi/webui:latest
outputs: type=docker,dest=/tmp/pavi_webui_docker_image.tar
- name: Upload image as artifact (share between jobs)
uses: actions/upload-artifact@v4
with:
name: webui_image
path: /tmp/pavi_webui_docker_image.tar
e2e-testing:
name: end-to-end testing
needs:
- webui-update-dependency-lock-files
- webui-container-image-build
- api-container-image-build
runs-on: ubuntu-22.04
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Download API image artifact
uses: actions/download-artifact@v4
with:
name: api_image
path: /tmp
- name: Load API Docker image
run: |
docker load --input /tmp/pavi_api_docker_image.tar
- name: Run local API container to run E2E tests on
working-directory: api/
run: |
make run-container-dev
- name: Download webUI image artifact
uses: actions/download-artifact@v4
with:
name: webui_image
path: /tmp
- name: Load webUI Docker image
run: |
docker load --input /tmp/pavi_webui_docker_image.tar
- name: Run local webUI container to run E2E tests on
working-directory: webui/
run: |
PAVI_API_PORT=8080 make run-container-dev
- name: Download updated webui dependencies lock file
uses: actions/download-artifact@v4
with:
name: webui_deps_lock
path: webui
- name: setup webui-compatible node.js version
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run cypress E2E tests
uses: cypress-io/github-action@v6
with:
working-directory: webui/
wait-on: 'http://localhost:3000, http://localhost:8080/api/health'
- name: Cleanup webUI server (running container)
working-directory: webui/
run: |
make stop-container-dev
- name: Cleanup API server (running container)
working-directory: api/
run: |
make stop-container-dev
stage-deps-lock-updates:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-deps-lock-updates') }}
runs-on: ubuntu-22.04
Expand Down
6 changes: 5 additions & 1 deletion api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ container-image:

run-container-dev:
export API_PIPELINE_IMAGE_TAG=main && \
docker compose -f docker-compose-dev.yml --env-file dev.env up agr.pavi.dev-local.api
docker compose -f docker-compose-dev.yml --env-file dev.env up -d agr.pavi.dev-local.api

stop-container-dev:
export API_PIPELINE_IMAGE_TAG=main && \
docker compose -f docker-compose-dev.yml --env-file dev.env down agr.pavi.dev-local.api

nextflow.sh:
make -f ../pipeline/workflow/Makefile nextflow.sh
Expand Down
1 change: 0 additions & 1 deletion shared_aws_infra/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
SUPPORTED_NODE := ^v18\.
EXTRA_PIP_COMPILE_ARGS ?=

.PHONY: check-% clean install install-% pip-tools run-% update-% _vars-% _write-lock-file
Expand Down
11 changes: 11 additions & 0 deletions webui/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,16 @@
"extends": [
"next/core-web-vitals",
"eslint:recommended"
],
"overrides": [
{
"files": ["**/__tests__/**/*.[jt]s?(x)"],
"plugins": [ "jest" ],
"env": { "jest/globals": true }
}, {
"files": ["cypress/**/*.cy.[jt]s?(x)"],
"plugins": [ "cypress" ],
"env": { "cypress/globals": true }
}
]
}
1 change: 1 addition & 0 deletions webui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# testing
/coverage
/cypress/screenshots/

# next.js
/.next/
Expand Down
19 changes: 18 additions & 1 deletion webui/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ container-image:
install-deps:
npm install

install-cypress-deps:
sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb

update-deps-lock:
npm update --package-lock-only

Expand All @@ -32,7 +35,7 @@ endif

run-container-dev:
@export PAVI_API_BASE_URL=${PAVI_API_BASE_URL} && \
docker compose -f docker-compose-dev.yml --env-file dev.env up agr.pavi.dev-local.webui
docker compose -f docker-compose-dev.yml --env-file dev.env up -d agr.pavi.dev-local.webui

run-server-dev: install-deps
@export PAVI_API_BASE_URL=${PAVI_API_BASE_URL} && \
Expand All @@ -43,3 +46,17 @@ run-style-checks: install-deps

run-type-checks: install-deps
npm run typecheck

run-unit-tests: install-deps
npm run test

run-e2e-tests: install-deps
$(MAKE) --no-print-directory run-container-dev
npx cypress run --e2e || true
$(MAKE) --no-print-directory stop-container-dev

run-e2e-tests-dev: install-deps
npx cypress open --e2e

stop-container-dev:
@docker compose -f docker-compose-dev.yml --env-file dev.env down agr.pavi.dev-local.webui
8 changes: 8 additions & 0 deletions webui/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: false
},
});
82 changes: 82 additions & 0 deletions webui/cypress/e2e/submit-workflow.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/// <reference types="cypress" />

// To learn more about how Cypress works,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress

import formInput from '../fixtures/test-submit-success-input.json'

describe('submit form behaviour', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command
// before each test
cy.visit('/')
})

it('tests job submission success', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// There should only be one cell with a inputgroup.
// cy.get('table tbody tr td .p-inputgroup').should('have.length', 1)

// Should displays one alignmentEntry by default.
cy.get('.p-inputgroup').should('have.length', 1)

// There should be excactly one submit button
cy.get('button').filter('[aria-label="Submit"]').as('submitBtn')
cy.get('@submitBtn').should('have.length', 1)

// and it should be disabled by default (on incomplete input).
cy.get('@submitBtn').should('be.disabled')

// There should be excactly one element to click to add records
cy.get('button#add-record').as('addRecordBtn')
cy.get('@addRecordBtn').should('have.length', 1)

// add as many records as there are entries in formInput
for(let i = 1, len = formInput.length; i < len; ++i){
cy.get('@addRecordBtn').click()
}
cy.get('.p-inputgroup').should('have.length', formInput.length)

// Input all data into form
for(let i = 0, len = formInput.length; i < len; ++i){

// Form should be able to receive gene as user input.
cy.get('.p-inputgroup').eq(i).find('input#gene').focus().type(formInput[i].gene)

// Once the transcript list loaded, from should enable selecting the relevant transcripts.
cy.get('.p-inputgroup').eq(i).find('#transcripts').find('input').focus()
cy.get('.p-multiselect-panel').as('openTranscriptsSelectBox').should('be.visible')

// A list of transcript should be available
cy.get('@openTranscriptsSelectBox').find('li.p-multiselect-item').as('openTranscriptsList')
cy.get('@openTranscriptsList').should('have.length.at.least', 1)

// And the relevant transcripts should be selectable
formInput[i].transcripts.forEach((transcript) => {
cy.get('@openTranscriptsList').contains(transcript).click()
})
cy.get('@openTranscriptsSelectBox').find('button.p-multiselect-close').click()

cy.focused().blur()

// Submit button should stay disabled as long a last entry was not submitted
if ( i < len - 1 ) {
cy.get('@submitBtn').should('be.disabled')

if (i === 0) {
cy.wait(5000)
cy.get('@submitBtn').should('be.disabled')
}
}
}

// Submit button should become active after completing all input
cy.get('@submitBtn').should('be.enabled')

// Submitting the analysis should report a UUID
cy.get('@submitBtn').click()
cy.contains('div#display-message', /^job .+ is now pending\.$/)
})
})
143 changes: 143 additions & 0 deletions webui/cypress/example-e2e/1-getting-started/todo.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/// <reference types="cypress" />

// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress

describe('example to-do app', () => {
beforeEach(() => {
// Cypress starts out with a blank slate for each test
// so we must tell it to visit our website with the `cy.visit()` command.
// Since we want to visit the same URL at the start of all our tests,
// we include it in our beforeEach function so that it runs before each test
cy.visit('https://example.cypress.io/todo')
})

it('displays two todo items by default', () => {
// We use the `cy.get()` command to get all elements that match the selector.
// Then, we use `should` to assert that there are two matched items,
// which are the two default items.
cy.get('.todo-list li').should('have.length', 2)

// We can go even further and check that the default todos each contain
// the correct text. We use the `first` and `last` functions
// to get just the first and last matched elements individually,
// and then perform an assertion with `should`.
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})

it('can add new todo items', () => {
// We'll store our item text in a variable so we can reuse it
const newItem = 'Feed the cat'

// Let's get the input element and use the `type` command to
// input our new list item. After typing the content of our item,
// we need to type the enter key as well in order to submit the input.
// This input has a data-test attribute so we'll use that to select the
// element in accordance with best practices:
// https://on.cypress.io/selecting-elements
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)

// Now that we've typed our new item, let's check that it actually was added to the list.
// Since it's the newest item, it should exist as the last element in the list.
// In addition, with the two default items, we should have a total of 3 elements in the list.
// Since assertions yield the element that was asserted on,
// we can chain both of these assertions together into a single statement.
cy.get('.todo-list li')
.should('have.length', 3)
.last()
.should('have.text', newItem)
})

it('can check off an item as completed', () => {
// In addition to using the `get` command to get an element by selector,
// we can also use the `contains` command to get an element by its contents.
// However, this will yield the <label>, which is lowest-level element that contains the text.
// In order to check the item, we'll find the <input> element for this <label>
// by traversing up the dom to the parent element. From there, we can `find`
// the child checkbox <input> element and use the `check` command to check it.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()

// Now that we've checked the button, we can go ahead and make sure
// that the list element is now marked as completed.
// Again we'll use `contains` to find the <label> element and then use the `parents` command
// to traverse multiple levels up the dom until we find the corresponding <li> element.
// Once we get that element, we can assert that it has the completed class.
cy.contains('Pay electric bill')
.parents('li')
.should('have.class', 'completed')
})

context('with a checked task', () => {
beforeEach(() => {
// We'll take the command we used above to check off an element
// Since we want to perform multiple tests that start with checking
// one element, we put it in the beforeEach hook
// so that it runs at the start of every test.
cy.contains('Pay electric bill')
.parent()
.find('input[type=checkbox]')
.check()
})

it('can filter for uncompleted tasks', () => {
// We'll click on the "active" button in order to
// display only incomplete items
cy.contains('Active').click()

// After filtering, we can assert that there is only the one
// incomplete item in the list.
cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Walk the dog')

// For good measure, let's also assert that the task we checked off
// does not exist on the page.
cy.contains('Pay electric bill').should('not.exist')
})

it('can filter for completed tasks', () => {
// We can perform similar steps as the test above to ensure
// that only completed tasks are shown
cy.contains('Completed').click()

cy.get('.todo-list li')
.should('have.length', 1)
.first()
.should('have.text', 'Pay electric bill')

cy.contains('Walk the dog').should('not.exist')
})

it('can delete all completed tasks', () => {
// First, let's click the "Clear completed" button
// `contains` is actually serving two purposes here.
// First, it's ensuring that the button exists within the dom.
// This button only appears when at least one task is checked
// so this command is implicitly verifying that it does exist.
// Second, it selects the button so we can click it.
cy.contains('Clear completed').click()

// Then we can make sure that there is only one element
// in the list and our element does not exist
cy.get('.todo-list li')
.should('have.length', 1)
.should('not.have.text', 'Pay electric bill')

// Finally, make sure that the clear button no longer exists.
cy.contains('Clear completed').should('not.exist')
})
})
})
Loading