diff --git a/frontend/webapp/.gitignore b/frontend/webapp/.gitignore index ded334b2c..4e270c208 100644 --- a/frontend/webapp/.gitignore +++ b/frontend/webapp/.gitignore @@ -33,4 +33,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + .DS_Store +*.pid +*.log diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx index 8ce00ad1e..80eaeaf54 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx @@ -92,7 +92,7 @@ export const DestinationListItem: React.FC = ({ item, }; return ( - onSelect(item)}> + onSelect(item)}> destination diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx index 5ab9f18b2..41bb135ec 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx @@ -49,7 +49,7 @@ const DestinationsList: React.FC = ({ items, setSelectedI {item.items.map((categoryItem) => ( - + ))} ); diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/choose-sources-body-fast/sources-list/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/choose-sources-body-fast/sources-list/index.tsx index 600511e92..1b95461fc 100644 --- a/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/choose-sources-body-fast/sources-list/index.tsx +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-sources-body/choose-sources-body-fast/sources-list/index.tsx @@ -136,7 +136,7 @@ export const SourcesList: React.FC = ({ const hasFilteredSources = !!filtered.length; return ( - + onSelectNamespace(namespace)}> onSelectAll(bool, namespace)} /> diff --git a/frontend/webapp/cypress.config.ts b/frontend/webapp/cypress.config.ts index 46927c59c..a53e56af0 100644 --- a/frontend/webapp/cypress.config.ts +++ b/frontend/webapp/cypress.config.ts @@ -1,10 +1,12 @@ import Cypress from 'cypress'; +const PORT = 3000; +const BASE_URL = `http://localhost:${PORT}`; + const config: Cypress.ConfigOptions = { e2e: { - // this uses the "production" build, if you want to use the "development" build, you can use "port=3000" instead - baseUrl: 'http://localhost:3001', setupNodeEvents(on, config) {}, + baseUrl: BASE_URL, supportFile: false, waitForAnimations: true, }, diff --git a/frontend/webapp/cypress/e2e/01-connection.cy.ts b/frontend/webapp/cypress/e2e/01-connection.cy.ts new file mode 100644 index 000000000..c9fdd70c0 --- /dev/null +++ b/frontend/webapp/cypress/e2e/01-connection.cy.ts @@ -0,0 +1,13 @@ +describe('Root Connection', () => { + it('Should fetch a config with GraphQL. A redirect of any kind confirms Frontend + Backend connections.', () => { + cy.intercept('/graphql').as('gql'); + cy.visit('/'); + + cy.wait('@gql').then(() => { + cy.location().should((loc) => { + // If GraphQL failed to fetch the config, the app will remain on "/", thereby failing the test. + expect(loc.pathname).to.be.oneOf(['/choose-sources', '/overview']); + }); + }); + }); +}); diff --git a/frontend/webapp/cypress/e2e/02-onboarding.cy.ts b/frontend/webapp/cypress/e2e/02-onboarding.cy.ts new file mode 100644 index 000000000..a3ceafa1f --- /dev/null +++ b/frontend/webapp/cypress/e2e/02-onboarding.cy.ts @@ -0,0 +1,43 @@ +import { ROUTES } from '../../utils/constants/routes'; + +describe('Onboarding', () => { + it('Should contain at least a "default" namespace', () => { + cy.intercept('/graphql').as('gql'); + cy.visit(ROUTES.CHOOSE_SOURCES); + + cy.wait('@gql').then(() => { + expect('#namespace-default').to.exist; + }); + }); + + it('Should contain at least a "Jaeger" destination', () => { + cy.intercept('/graphql').as('gql'); + cy.visit(ROUTES.CHOOSE_DESTINATION); + + cy.wait('@gql').then(() => { + cy.contains('button', 'ADD DESTINATION').click(); + expect('#destination-jaeger').to.exist; + }); + }); + + it('Should autocomplete the "Jaeger" destination', () => { + cy.intercept('/graphql').as('gql'); + cy.visit(ROUTES.CHOOSE_DESTINATION); + + cy.wait('@gql').then(() => { + cy.contains('button', 'ADD DESTINATION').click(); + cy.get('#destination-jaeger').click(); + expect('#JAEGER_URL').to.not.be.empty; + }); + }); + + it('Should allow the user to pass every step, and end-up on the "Overview" page.', () => { + cy.visit(ROUTES.CHOOSE_SOURCES); + + cy.contains('button', 'NEXT').click(); + cy.location('pathname').should('eq', ROUTES.CHOOSE_DESTINATION); + + cy.contains('button', 'DONE').click(); + cy.location('pathname').should('eq', ROUTES.OVERVIEW); + }); +}); diff --git a/frontend/webapp/cypress/e2e/onboarding.cy.ts b/frontend/webapp/cypress/e2e/onboarding.cy.ts deleted file mode 100644 index e8d975e58..000000000 --- a/frontend/webapp/cypress/e2e/onboarding.cy.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('Onboarding', () => { - it('Visiting the root path fetches a config with GraphQL. A fresh install will result in a redirect to the start of onboarding, confirming Front + Back connections', () => { - cy.visit('/'); - // If backend connection failed for any reason, teh default redirect would be "/overview" - cy.location('pathname').should('eq', '/choose-sources'); - }); -}); diff --git a/frontend/webapp/package.json b/frontend/webapp/package.json index b144f7bcf..9fdd896f0 100644 --- a/frontend/webapp/package.json +++ b/frontend/webapp/package.json @@ -3,15 +3,11 @@ "version": "0.1.0", "private": true, "scripts": { - "back:build": "cd .. && go build -o ./odigos-backend", - "back:start": "cd .. && ./odigos-backend --port 8085 --debug --address 0.0.0.0", - "predev": "rm -rf .next", "dev": "next dev", - "prebuild": "rm -rf out", "build": "next build", "start": "next start", "lint": "next lint --fix", - "cy": "cypress run --e2e -q", + "cy:run": "cypress run --e2e -q", "cy:open": "cypress open --e2e -b electron" }, "dependencies": { @@ -34,11 +30,10 @@ "@types/react-dom": "18.3.1", "autoprefixer": "^10.4.20", "babel-plugin-styled-components": "^2.1.4", - "cypress": "^13.16.0", + "cypress": "^13.16.1", "eslint": "9.15.0", "eslint-config-next": "15.0.3", "postcss": "^8.4.49", "typescript": "5.6.3" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } diff --git a/frontend/webapp/reuseable-components/input/index.tsx b/frontend/webapp/reuseable-components/input/index.tsx index 4c20ce254..3930769d9 100644 --- a/frontend/webapp/reuseable-components/input/index.tsx +++ b/frontend/webapp/reuseable-components/input/index.tsx @@ -115,7 +115,7 @@ const Button = styled.button` // Wrap Input with forwardRef to handle the ref prop const Input = forwardRef( - ({ icon, buttonLabel, onButtonClick, hasError, errorMessage, title, tooltip, required, initialValue, onChange, type = 'text', ...props }, ref) => { + ({ icon, buttonLabel, onButtonClick, hasError, errorMessage, title, tooltip, required, initialValue, onChange, type = 'text', name, ...props }, ref) => { const isSecret = type === 'password'; const [revealSecret, setRevealSecret] = useState(false); const [value, setValue] = useState(initialValue || ''); @@ -144,6 +144,8 @@ const Input = forwardRef( = ({ title = 'No data found', subTitle = 'Check your search phrase and try one more time' }) => { return ( - + no-found {title} diff --git a/frontend/webapp/reuseable-components/textarea/index.tsx b/frontend/webapp/reuseable-components/textarea/index.tsx index cd9faeea0..fc71d54e8 100644 --- a/frontend/webapp/reuseable-components/textarea/index.tsx +++ b/frontend/webapp/reuseable-components/textarea/index.tsx @@ -79,7 +79,7 @@ const StyledTextArea = styled.textarea` } `; -const TextArea: React.FC = ({ errorMessage, title, tooltip, required, onChange, ...props }) => { +const TextArea: React.FC = ({ errorMessage, title, tooltip, required, onChange, name, ...props }) => { const ref = useRef(null); const resize = () => { @@ -97,6 +97,8 @@ const TextArea: React.FC = ({ errorMessage, title, tooltip, requi { diff --git a/frontend/webapp/yarn.lock b/frontend/webapp/yarn.lock index 185b1ddac..200d61fe5 100644 --- a/frontend/webapp/yarn.lock +++ b/frontend/webapp/yarn.lock @@ -3123,10 +3123,10 @@ csstype@3.1.3, csstype@^3.0.2, csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -cypress@^13.16.0: - version "13.16.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.16.0.tgz#7674ca33941f9da58f15fd4e3456856d87730970" - integrity sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA== +cypress@^13.16.1: + version "13.16.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.16.1.tgz#82e776f6ad2037ccce6b6feabed768615c476258" + integrity sha512-17FtCaz0cx7ssWYKXzGB0Vub8xHwpVPr+iPt2fHhLMDhVAPVrplD+rTQsZUsfb19LVBn5iwkEUFjQ1yVVJXsLA== dependencies: "@cypress/request" "^3.0.6" "@cypress/xvfb" "^1.2.4" diff --git a/tests/common/.gitignore b/tests/common/.gitignore deleted file mode 100644 index 10327c64f..000000000 --- a/tests/common/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pid -*.log \ No newline at end of file diff --git a/tests/common/odigos_ui.sh b/tests/common/odigos_ui.sh deleted file mode 100755 index bf187c67b..000000000 --- a/tests/common/odigos_ui.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -# Ensure the script fails if any command fails -set -e - -scripts_dir="$(cd "$(dirname "$0")" && pwd)" -# The above "$scripts_dir" key is used to identify where the script was called from, to ensure all paths are relative to the script. -# This is useful when the script is called from another location, and the paths are relative to the calling script (for exmaple YAML file). - -log_file="$scripts_dir/odigos_ui.log" -pid_file="$scripts_dir/odigos_ui.pid" - -function get_process_id() { - if [ ! -f "$1" ]; then - # File does not exist - echo "0" - return - fi - - pid=$(cat "$1" 2>/dev/null) - if ps -p "$pid" > /dev/null 2>&1; then - # Process is running - echo "$pid" - else - # Process is not running - echo "0" - fi -} - -function check_process() { - # Check if the process is running - if [ "$1" == 0 ]; then - echo "Odigos UI - ❌ Failed to start" - cat "$log_file" - exit 1 - else - echo "Odigos UI - ✅ Ready" - cat "$log_file" - fi -} - -function kill_process() { - # Kill the process - if [ "$1" != 0 ]; then - echo "Odigos UI - 💀 Killing process ($1)" - kill $1 - fi -} - -function kill_all() { - pid=$(get_process_id "$pid_file") - kill_process $pid -} - -function stop() { - kill_all - - # Cleanup - rm -f "$log_file" - rm -f "$pid_file" -} - -function start() { - kill_all - cd "$scripts_dir/../../frontend/webapp" - - # Install dependencies - echo "Odigos UI - ⏳ Installing..." - yarn install > /dev/null 2> "$log_file" - - # Create a production build - echo "Odigos UI - ⏳ Building..." - yarn build > /dev/null 2> "$log_file" - yarn back:build > /dev/null 2> "$log_file" - - # Start the production build - echo "Odigos UI - ⏳ Starting..." - cd .. - ./odigos-backend --port 8085 --debug --address 0.0.0.0 > /dev/null 2> "$log_file" & - - sleep 1 - echo $! > "$pid_file" - pid=$(get_process_id "$pid_file") - check_process $pid -} - -function test() { - # Run tests on the Frontend - cd "$scripts_dir/../../frontend/webapp" - echo "Odigos UI - 👀 Testing with Cypress..." - - set +e # Temporarily disable "exit on error" - yarn cy - test_exit_code=$? - set -e # Re-enable "exit on error" - - if [ $test_exit_code -ne 0 ]; then - echo "Odigos UI - ❌ Cypress tests failed" - exit 1 - else - echo "Odigos UI - ✅ Cypress tests passed" - fi -} - -# This is to allow the script to be used dynamically, we call the function name from the CLI (start/stop/test/etc.) -# This method prevents duplicated code across multiple-scripts -function main() { - if [ $# -lt 1 ]; then - echo "❌ Error: Incorrect usage - '$0 '" - exit 1 - fi - - func="$1" - shift # Shift arguments, so $@ contains only the arguments for the function - - # Check if the function exists and call it (with the remaining arguments) - if declare -f "$func" > /dev/null; then - $func "$@" - else - echo "❌ Error: Function '$func' not found." - exit 1 - fi -} - -main "$@" diff --git a/tests/e2e/ui/chainsaw-test.yaml b/tests/e2e/ui/chainsaw-test.yaml index af2936c42..87ca791e0 100644 --- a/tests/e2e/ui/chainsaw-test.yaml +++ b/tests/e2e/ui/chainsaw-test.yaml @@ -1,23 +1,63 @@ apiVersion: chainsaw.kyverno.io/v1alpha1 kind: Test metadata: - name: ui-cypress + name: ui spec: description: Run E2E tests against Odigos UI using Cypress skipDelete: true steps: - - name: Start the UI + - name: Install Odigos CLI try: - script: - timeout: 300s - content: ../../common/odigos_ui.sh start - - name: Test the UI + timeout: 60s + content: | + ../../../cli/odigos install --version e2e-test + + - name: Install App - Simple Demo try: + - apply: + file: ../../common/apply/install-simple-demo.yaml - script: - timeout: 300s - content: ../../common/odigos_ui.sh test - - name: End the UI + timeout: 120s + content: | + kubectl wait --for=condition=ready pod -l app=frontend --timeout=120s + kubectl wait --for=condition=ready pod -l app=coupon --timeout=120s + kubectl wait --for=condition=ready pod -l app=inventory --timeout=120s + kubectl wait --for=condition=ready pod -l app=pricing --timeout=120s + kubectl wait --for=condition=ready pod -l app=membership --timeout=120s + - assert: + file: ../../common/assert/simple-demo-installed.yaml + + - name: Add Destination - Jaeger try: - script: - timeout: 60s - content: ../../common/odigos_ui.sh stop + timeout: 120s + content: | + kubectl apply -f https://raw.githubusercontent.com/odigos-io/simple-demo/main/kubernetes/jaeger.yaml + kubectl wait --for=condition=available --timeout=120s deployment/jaeger -n tracing + + - name: Start UI from CLI + try: + - script: + timeout: 10s + content: | + nohup ../../../cli/odigos ui --beta > odigos-ui.log 2>&1 & + sleep 5 + + - name: Wait for UI + try: + - script: + timeout: 30s + content: | + for i in {1..10}; do + curl -s http://localhost:3000 && break || sleep 2 + done + + - name: Run Cypress tests + try: + - script: + timeout: 300s + content: | + cd ../../../frontend/webapp + yarn install + yarn cy:run