From 7bc0f32145369f4f35bd247bb9e46918f36888bf Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 8 Jul 2025 17:16:57 +0200 Subject: [PATCH] Added cypress tests --- Makefile | 82 ++++++++ app/.gitignore | 2 + app/cypress.config.js | 19 ++ app/cypress/e2e/container.spec.js | 90 +++++++++ app/cypress/e2e/fullscreen.spec.js | 85 ++++++++ app/cypress/e2e/fullwidth.spec.js | 61 ++++++ app/cypress/e2e/iframe.spec.js | 46 +++++ app/cypress/e2e/menu.spec.js | 130 ++++++++++++ app/cypress/e2e/modal.spec.js | 130 ++++++++++++ app/cypress/e2e/tooltips.spec.js | 130 ++++++++++++ app/package.json | 5 + app/static/js/fullscreen.js | 14 +- app/static/js/iframe.js | 81 ++++---- env.example | 2 +- main.py | 308 +++++------------------------ 15 files changed, 879 insertions(+), 306 deletions(-) create mode 100644 Makefile create mode 100644 app/.gitignore create mode 100644 app/cypress.config.js create mode 100644 app/cypress/e2e/container.spec.js create mode 100644 app/cypress/e2e/fullscreen.spec.js create mode 100644 app/cypress/e2e/fullwidth.spec.js create mode 100644 app/cypress/e2e/iframe.spec.js create mode 100644 app/cypress/e2e/menu.spec.js create mode 100644 app/cypress/e2e/modal.spec.js create mode 100644 app/cypress/e2e/tooltips.spec.js create mode 100644 app/package.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0908cda --- /dev/null +++ b/Makefile @@ -0,0 +1,82 @@ +# Load environment variables from .env +ifneq (,$(wildcard .env)) + include .env + # Export variables defined in .env + export $(shell sed 's/=.*//' .env) +endif + +# Default port (can be overridden with PORT env var) +PORT ?= 5000 + +# Default port (can be overridden with PORT env var) +.PHONY: build +build: + # Build the Docker image. + docker build -t application-portfolio . + +.PHONY: up +up: + # Start the application using docker-compose with build. + docker-compose up -d --build + +.PHONY: down +down: + # Stop and remove the 'portfolio' container, ignore errors, and bring down compose. + - docker stop portfolio || true + - docker rm portfolio || true + - docker-compose down + +.PHONY: run-dev +run-dev: + # Run the container in development mode (hot-reload). + docker run -d \ + -p $(PORT):$(PORT) \ + --name portfolio \ + -v $(PWD)/app/:/app \ + -e FLASK_APP=app.py \ + -e FLASK_ENV=development \ + application-portfolio + +.PHONY: run-prod +run-prod: + # Run the container in production mode. + docker run -d \ + -p $(PORT):$(PORT) \ + --name portfolio \ + application-portfolio + +.PHONY: logs +logs: + # Display the logs of the 'portfolio' container. + docker logs -f portfolio + +.PHONY: dev +dev: + # Start the application in development mode using docker-compose. + FLASK_ENV=development docker-compose up -d + +.PHONY: prod +prod: + # Start the application in production mode using docker-compose (with build). + docker-compose up -d --build + +.PHONY: cleanup +cleanup: + # Remove all stopped Docker containers to reclaim space. + docker container prune -f + +.PHONY: delete +delete: + # Force remove the 'portfolio' container if it exists. + - docker rm -f portfolio + +.PHONY: browse +browse: + # Open the application in the browser at http://localhost:$(PORT) + chromium http://localhost:$(PORT) + +# Cypress tests\ nCYPRESS_DIR := app +.PHONY: test +test: down prod + # Run end-to-end tests with Cypress. + cd app && npx cypress run --spec "cypress/e2e/**/*.spec.js" diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..ccb2c80 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json \ No newline at end of file diff --git a/app/cypress.config.js b/app/cypress.config.js new file mode 100644 index 0000000..a2155be --- /dev/null +++ b/app/cypress.config.js @@ -0,0 +1,19 @@ +// cypress.config.js +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + e2e: { + // your app under test must already be running on this port + baseUrl: `http://localhost:${process.env.PORT || 5001}`, + defaultCommandTimeout: 60000, + pageLoadTimeout: 60000, + requestTimeout: 1500, + responseTimeout: 15000, + specPattern: 'cypress/e2e/**/*.spec.js', + supportFile: false, + setupNodeEvents(on, config) { + // here you could hook into events, but we don’t need anything special + return config; + } + }, +}); diff --git a/app/cypress/e2e/container.spec.js b/app/cypress/e2e/container.spec.js new file mode 100644 index 0000000..7ecb10c --- /dev/null +++ b/app/cypress/e2e/container.spec.js @@ -0,0 +1,90 @@ +// cypress/e2e/container.spec.js + +describe('Custom Scroll & Container Resizing', () => { + beforeEach(() => { + // Assumes your app is running at baseUrl, and container.js is loaded on “/” + cy.visit('/'); + }); + + it('on load, the scroll-container gets a positive height and proper overflow', () => { + // wait for our JS to run + cy.window().should('have.property', 'adjustScrollContainerHeight'); + + // Grab the inline style of .scroll-container + cy.get('.scroll-container') + .should('have.attr', 'style') + .then(style => { + // height:px must be present + const m = style.match(/height:\s*(\d+(?:\.\d+)?)px/); + expect(m, 'height set').to.not.be.null; + expect(parseFloat(m[1]), 'height > 0').to.be.greaterThan(0); + + // overflow shorthand should include both hidden & auto (order-insensitive) + expect(style).to.include('overflow:'); + expect(style).to.match(/overflow:\s*(hidden\s+auto|auto\s+hidden)/); + }); + }); + + it('on window resize, scroll-container height updates', () => { + // record original height + cy.get('.scroll-container') + .invoke('css', 'height') + .then(orig => { + // resize to a smaller viewport + cy.viewport(320, 480); + cy.wait(100); // allow resize handler to fire + + cy.get('.scroll-container') + .invoke('css', 'height') + .then(newH => { + expect(parseFloat(newH), 'height changed on resize').to.not.equal(parseFloat(orig)); + }); + }); + }); + + context('custom scrollbar thumb', () => { + beforeEach(() => { + // inject tall content to force scrolling + cy.get('.scroll-container').then($sc => { + $sc[0].innerHTML = '
long
'; + }); + // re-run scrollbar setup + cy.window().invoke('updateCustomScrollbar'); + }); + + it('shows a thumb with reasonable size & position', () => { + cy.get('#custom-scrollbar').should('have.css', 'opacity', '1'); + + cy.get('#scroll-thumb') + .should('have.css', 'height') + .then(h => { + const hh = parseFloat(h); + expect(hh).to.be.at.least(20); + // ensure thumb is smaller than container + cy.get('#custom-scrollbar') + .invoke('css', 'height') + .then(ch => { + expect(hh).to.be.lessThan(parseFloat(ch)); + }); + }); + + // scroll a bit and verify thumb.top changes + cy.get('.scroll-container').scrollTo(0, 200); + cy.wait(50); + cy.get('#scroll-thumb') + .invoke('css', 'top') + .then(t => { + expect(parseFloat(t)).to.be.greaterThan(0); + }); + }); + + it('hides scrollbar when content fits', () => { + // remove overflow + cy.get('.scroll-container').then($sc => { + $sc[0].innerHTML = '
tiny
'; + }); + cy.window().invoke('updateCustomScrollbar'); + cy.get('#custom-scrollbar').should('have.css', 'opacity', '0'); + }); + }); +}); diff --git a/app/cypress/e2e/fullscreen.spec.js b/app/cypress/e2e/fullscreen.spec.js new file mode 100644 index 0000000..e6626a0 --- /dev/null +++ b/app/cypress/e2e/fullscreen.spec.js @@ -0,0 +1,85 @@ +// cypress/e2e/fullscreen.spec.js + +describe('Fullscreen Toggle', () => { + const ROOT = '/'; + + beforeEach(() => { + cy.visit(ROOT); + }); + + it('defaults to normal mode when no fullscreen param is present', () => { + // Body should not have fullscreen class + cy.get('body').should('not.have.class', 'fullscreen'); + + // URL should not include `fullscreen` + cy.url().should('not.include', 'fullscreen='); + + // Header and footer should be visible (max-height > 0) + cy.get('header').should('have.css', 'max-height').and(value => { + expect(parseFloat(value)).to.be.greaterThan(0); + }); + cy.get('footer').should('have.css', 'max-height').and(value => { + expect(parseFloat(value)).to.be.greaterThan(0); + }); + }); + + it('initFullscreenFromUrl() picks up ?fullscreen=1 on load', () => { + cy.visit(`${ROOT}?fullscreen=1`); + + cy.get('body').should('have.class', 'fullscreen'); + cy.url().should('include', 'fullscreen=1'); + + // Header and footer should be collapsed (max-height == 0) + cy.get('header').should('have.css', 'max-height', '0px'); + cy.get('footer').should('have.css', 'max-height', '0px'); + }); + + it('enterFullscreen() adds fullscreen class, sets full width, and updates URL', () => { + cy.window().then(win => { + win.exitFullscreen(); // ensure starting state + win.enterFullscreen(); + }); + + cy.get('body').should('have.class', 'fullscreen'); + cy.url().should('include', 'fullscreen=1'); + cy.get('.container, .container-fluid') + .should('have.class', 'container-fluid'); + + cy.get('header').should('have.css', 'max-height', '0px'); + cy.get('footer').should('have.css', 'max-height', '0px'); + }); + + it('exitFullscreen() removes fullscreen class, resets width, and URL param', () => { + // start in fullscreen + cy.window().invoke('enterFullscreen'); + + // then exit + cy.window().invoke('exitFullscreen'); + + cy.get('body').should('not.have.class', 'fullscreen'); + cy.url().should('not.include', 'fullscreen='); + cy.get('.container, .container-fluid') + .should('have.class', 'container') + .and('not.have.class', 'container-fluid'); + + // Header and footer should be expanded again + cy.get('header').should('have.css', 'max-height').and(value => { + expect(parseFloat(value)).to.be.greaterThan(0); + }); + cy.get('footer').should('have.css', 'max-height').and(value => { + expect(parseFloat(value)).to.be.greaterThan(0); + }); + }); + + it('toggleFullscreen() toggles into and out of fullscreen', () => { + // Toggle into fullscreen + cy.window().invoke('toggleFullscreen'); + cy.get('body').should('have.class', 'fullscreen'); + cy.url().should('include', 'fullscreen=1'); + + // Toggle back + cy.window().invoke('toggleFullscreen'); + cy.get('body').should('not.have.class', 'fullscreen'); + cy.url().should('not.include', 'fullscreen='); + }); +}); diff --git a/app/cypress/e2e/fullwidth.spec.js b/app/cypress/e2e/fullwidth.spec.js new file mode 100644 index 0000000..cae153b --- /dev/null +++ b/app/cypress/e2e/fullwidth.spec.js @@ -0,0 +1,61 @@ +// cypress/e2e/fullwidth.spec.js + +describe('Full-width Toggle', () => { + // test page must include your
wrapper + const ROOT = '/'; + + it('defaults to .container when no param is present', () => { + cy.visit(ROOT); + cy.get('.container, .container-fluid') + .should('have.class', 'container') + .and('not.have.class', 'container-fluid'); + + // URL should not include `fullwidth` + cy.url().should('not.include', 'fullwidth='); + }); + + it('initFullWidthFromUrl() picks up ?fullwidth=1 on load', () => { + cy.visit(`${ROOT}?fullwidth=1`); + cy.get('.container, .container-fluid') + .should('have.class', 'container-fluid') + .and('not.have.class', 'container'); + cy.url().should('include', 'fullwidth=1'); + }); + + it('setFullWidth(true) switches to container-fluid and updates URL', () => { + cy.visit(ROOT); + + // call your global function + cy.window().invoke('setFullWidth', true); + + cy.get('.container, .container-fluid') + .should('have.class', 'container-fluid') + .and('not.have.class', 'container'); + + cy.url().should('include', 'fullwidth=1'); + }); + + it('setFullWidth(false) reverts to container and removes URL param', () => { + cy.visit(`${ROOT}?fullwidth=1`); + + // now reset + cy.window().invoke('setFullWidth', false); + + cy.get('.container, .container-fluid') + .should('have.class', 'container') + .and('not.have.class', 'container-fluid'); + + cy.url().should('not.include', 'fullwidth=1'); + }); + + it('updateUrlFullWidth() toggles the query param without changing layout', () => { + cy.visit(ROOT); + + // manually toggle URL only + cy.window().invoke('updateUrlFullWidth', true); + cy.url().should('include', 'fullwidth=1'); + + cy.window().invoke('updateUrlFullWidth', false); + cy.url().should('not.include', 'fullwidth='); + }); +}); diff --git a/app/cypress/e2e/iframe.spec.js b/app/cypress/e2e/iframe.spec.js new file mode 100644 index 0000000..6ec4fd3 --- /dev/null +++ b/app/cypress/e2e/iframe.spec.js @@ -0,0 +1,46 @@ +// cypress/e2e/iframe.spec.js + +describe('Iframe integration', () => { + beforeEach(() => { + // Visit the app’s base URL (configured in cypress.config.js) + cy.visit('/'); + }); + + it('opens the iframe when an .iframe-link is clicked', () => { + // Find the first iframe-link on the page + cy.get('.iframe-link').first().then($link => { + const href = $link.prop('href'); + + // Click it + cy.wrap($link).click(); + + // The URL should now include ?iframe= + cy.url().should('include', 'iframe=' + encodeURIComponent(href)); + + // The should have the "fullscreen" class + cy.get('body').should('have.class', 'fullscreen'); + + // And the
should contain a visible