i'm about to rework so much better save
This commit is contained in:
		
						commit
						8bb5fee1c0
					
				
					 45 changed files with 4600 additions and 0 deletions
				
			
		
							
								
								
									
										16
									
								
								.devcontainer/Dockerfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.devcontainer/Dockerfile
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# [Choice] Node.js version: 16, 18, 20
 | 
				
			||||||
 | 
					ARG VARIANT="22"
 | 
				
			||||||
 | 
					FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:1-${VARIANT}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# [Optional] Uncomment this section to install additional OS packages.
 | 
				
			||||||
 | 
					# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
 | 
				
			||||||
 | 
					#     && apt-get -y install --no-install-recommends <your-package-list-here>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# [Optional] Uncomment if you want to install an additional version of node using nvm
 | 
				
			||||||
 | 
					# ARG EXTRA_NODE_VERSION=10
 | 
				
			||||||
 | 
					# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# To install more global node packages
 | 
				
			||||||
 | 
					RUN su node -c "npm install -g pnpm@10 ts-node"
 | 
				
			||||||
							
								
								
									
										31
									
								
								.devcontainer/devcontainer.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.devcontainer/devcontainer.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
 | 
				
			||||||
 | 
					// https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "School Project",
 | 
				
			||||||
 | 
					  "build": {
 | 
				
			||||||
 | 
					    "dockerfile": "Dockerfile",
 | 
				
			||||||
 | 
					    // Update 'VARIANT' to pick a Node version: 16, 18, 20
 | 
				
			||||||
 | 
					    "args": {
 | 
				
			||||||
 | 
					      "VARIANT": "22"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  // Use 'forwardPorts' to make a list of ports inside the container available locally.
 | 
				
			||||||
 | 
					  "forwardPorts": [3000, 4321],
 | 
				
			||||||
 | 
					  // Use 'postCreateCommand' to run commands after the container is created.
 | 
				
			||||||
 | 
					  "postCreateCommand": "pnpm install",
 | 
				
			||||||
 | 
					  // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
 | 
				
			||||||
 | 
					  "remoteUser": "node",
 | 
				
			||||||
 | 
					  "customizations": {
 | 
				
			||||||
 | 
					    "vscode": {
 | 
				
			||||||
 | 
					      "extensions": [
 | 
				
			||||||
 | 
					        "astro-build.astro-vscode",
 | 
				
			||||||
 | 
					        "denoland.vscode-deno",
 | 
				
			||||||
 | 
					        "esbenp.prettier-vscode",
 | 
				
			||||||
 | 
					        "bradlc.vscode-tailwindcss",
 | 
				
			||||||
 | 
					        // Optional tbh
 | 
				
			||||||
 | 
					        "christian-kohler.path-intellisense",
 | 
				
			||||||
 | 
					        "YoavBls.pretty-ts-errors"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# dependencies
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					.pnpm-store
 | 
				
			||||||
 | 
					/.pnp
 | 
				
			||||||
 | 
					.pnp.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# testing
 | 
				
			||||||
 | 
					/coverage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# database
 | 
				
			||||||
 | 
					/prisma/db.sqlite
 | 
				
			||||||
 | 
					/prisma/db.sqlite-journal
 | 
				
			||||||
 | 
					db.sqlite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# next.js
 | 
				
			||||||
 | 
					/.next/
 | 
				
			||||||
 | 
					/out/
 | 
				
			||||||
 | 
					next-env.d.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# production
 | 
				
			||||||
 | 
					build/
 | 
				
			||||||
 | 
					.next
 | 
				
			||||||
 | 
					# misc
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					*.pem
 | 
				
			||||||
 | 
					db-database/
 | 
				
			||||||
 | 
					# debug
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					.pnpm-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# local env files
 | 
				
			||||||
 | 
					# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# vercel
 | 
				
			||||||
 | 
					.vercel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# typescript
 | 
				
			||||||
 | 
					*.tsbuildinfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# idea files
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
							
								
								
									
										15
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "sqltools.connections": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "previewLimit": 50,
 | 
				
			||||||
 | 
					            "server": "localhost",
 | 
				
			||||||
 | 
					            "driver": "PostgreSQL",
 | 
				
			||||||
 | 
					            "name": "School Project",
 | 
				
			||||||
 | 
					            "connectString": "postgresql://postgres:postgres@db.schoolproject.orb.local:5432/schoolproject",
 | 
				
			||||||
 | 
					            "database": "schoolproject",
 | 
				
			||||||
 | 
					            "username": "postgres",
 | 
				
			||||||
 | 
					            "password": "postgres"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "sqltools.results.reuseTabs": "connection"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								Backend/.env.example
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Backend/.env.example
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					# Since the ".env" file is gitignored, you can use the ".env.example" file to
 | 
				
			||||||
 | 
					# build a new ".env" file when you clone the repo. Keep this file up-to-date
 | 
				
			||||||
 | 
					# when you add new variables to `.env`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This file will be committed to version control, so make sure not to have any
 | 
				
			||||||
 | 
					# secrets in it. If you are cloning this repo, create a copy of this file named
 | 
				
			||||||
 | 
					# ".env" and populate it with your secrets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# When adding additional environment variables, the schema in "/src/env.js"
 | 
				
			||||||
 | 
					# should be updated accordingly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Example:
 | 
				
			||||||
 | 
					# SERVERVAR="foo"
 | 
				
			||||||
 | 
					# NEXT_PUBLIC_CLIENTVAR="bar"
 | 
				
			||||||
							
								
								
									
										29
									
								
								Backend/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Backend/README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					# Create T3 App
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## What's next? How do I make an app with this?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Next.js](https://nextjs.org)
 | 
				
			||||||
 | 
					- [NextAuth.js](https://next-auth.js.org)
 | 
				
			||||||
 | 
					- [Prisma](https://prisma.io)
 | 
				
			||||||
 | 
					- [Drizzle](https://orm.drizzle.team)
 | 
				
			||||||
 | 
					- [Tailwind CSS](https://tailwindcss.com)
 | 
				
			||||||
 | 
					- [tRPC](https://trpc.io)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Learn More
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Documentation](https://create.t3.gg/)
 | 
				
			||||||
 | 
					- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## How do I deploy this?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
 | 
				
			||||||
							
								
								
									
										26
									
								
								Backend/biome.jsonc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Backend/biome.jsonc
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
 | 
				
			||||||
 | 
						"vcs": {
 | 
				
			||||||
 | 
							"enabled": true,
 | 
				
			||||||
 | 
							"clientKind": "git",
 | 
				
			||||||
 | 
							"useIgnoreFile": true
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"files": { "ignoreUnknown": false, "ignore": [] },
 | 
				
			||||||
 | 
						"formatter": { "enabled": true },
 | 
				
			||||||
 | 
						"organizeImports": { "enabled": true },
 | 
				
			||||||
 | 
						"linter": {
 | 
				
			||||||
 | 
							"enabled": true,
 | 
				
			||||||
 | 
							"rules": {
 | 
				
			||||||
 | 
								"nursery": {
 | 
				
			||||||
 | 
									"useSortedClasses": {
 | 
				
			||||||
 | 
										"level": "warn",
 | 
				
			||||||
 | 
										"fix": "safe",
 | 
				
			||||||
 | 
										"options": {
 | 
				
			||||||
 | 
											"functions": ["clsx", "cva", "cn"]
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"recommended": true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								Backend/components.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Backend/components.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "https://ui.shadcn.com/schema.json",
 | 
				
			||||||
 | 
					  "style": "new-york",
 | 
				
			||||||
 | 
					  "rsc": true,
 | 
				
			||||||
 | 
					  "tsx": true,
 | 
				
			||||||
 | 
					  "tailwind": {
 | 
				
			||||||
 | 
					    "config": "",
 | 
				
			||||||
 | 
					    "css": "src/styles/globals.css",
 | 
				
			||||||
 | 
					    "baseColor": "zinc",
 | 
				
			||||||
 | 
					    "cssVariables": true,
 | 
				
			||||||
 | 
					    "prefix": ""
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aliases": {
 | 
				
			||||||
 | 
					    "components": "@/components",
 | 
				
			||||||
 | 
					    "utils": "@/lib/utils",
 | 
				
			||||||
 | 
					    "ui": "@/components/ui",
 | 
				
			||||||
 | 
					    "lib": "@/lib",
 | 
				
			||||||
 | 
					    "hooks": "@/hooks"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "iconLibrary": "lucide"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								Backend/next.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Backend/next.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
 | 
				
			||||||
 | 
					 * for Docker builds.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import "./src/env.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {import("next").NextConfig} */
 | 
				
			||||||
 | 
					const config = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||
							
								
								
									
										48
									
								
								Backend/package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								Backend/package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"name": "SchoolProject",
 | 
				
			||||||
 | 
						"version": "0.1.0",
 | 
				
			||||||
 | 
						"private": true,
 | 
				
			||||||
 | 
						"type": "module",
 | 
				
			||||||
 | 
						"scripts": {
 | 
				
			||||||
 | 
							"build": "next build",
 | 
				
			||||||
 | 
							"check": "biome check .",
 | 
				
			||||||
 | 
							"check:unsafe": "biome check --write --unsafe .",
 | 
				
			||||||
 | 
							"check:write": "biome check --write .",
 | 
				
			||||||
 | 
							"dev": "next dev --turbo",
 | 
				
			||||||
 | 
							"preview": "next build && next start",
 | 
				
			||||||
 | 
							"start": "next start",
 | 
				
			||||||
 | 
							"typecheck": "tsc --noEmit"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"@radix-ui/react-collapsible": "^1.1.10",
 | 
				
			||||||
 | 
							"@radix-ui/react-slot": "^1.2.2",
 | 
				
			||||||
 | 
							"@t3-oss/env-nextjs": "^0.12.0",
 | 
				
			||||||
 | 
							"@tanstack/react-query": "^5.76.1",
 | 
				
			||||||
 | 
							"class-variance-authority": "^0.7.1",
 | 
				
			||||||
 | 
							"clsx": "^2.1.1",
 | 
				
			||||||
 | 
							"lucide-react": "^0.509.0",
 | 
				
			||||||
 | 
							"next": "^15.2.3",
 | 
				
			||||||
 | 
							"next-themes": "^0.4.6",
 | 
				
			||||||
 | 
							"pg": "^8.14.1",
 | 
				
			||||||
 | 
							"react": "^19.0.0",
 | 
				
			||||||
 | 
							"react-dom": "^19.0.0",
 | 
				
			||||||
 | 
							"tailwind-merge": "^3.2.0",
 | 
				
			||||||
 | 
							"zod": "^3.24.2"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@biomejs/biome": "1.9.4",
 | 
				
			||||||
 | 
							"@tailwindcss/postcss": "^4.0.15",
 | 
				
			||||||
 | 
							"@types/node": "^20.14.10",
 | 
				
			||||||
 | 
							"@types/pg": "^8.11.13",
 | 
				
			||||||
 | 
							"@types/react": "^19.0.0",
 | 
				
			||||||
 | 
							"@types/react-dom": "^19.0.0",
 | 
				
			||||||
 | 
							"postcss": "^8.5.3",
 | 
				
			||||||
 | 
							"tailwindcss": "^4.0.15",
 | 
				
			||||||
 | 
							"tw-animate-css": "^1.2.9",
 | 
				
			||||||
 | 
							"typescript": "^5.8.2"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"ct3aMetadata": {
 | 
				
			||||||
 | 
							"initVersion": "7.39.3"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"packageManager": "pnpm@10.8.0"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1485
									
								
								Backend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1485
									
								
								Backend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										3
									
								
								Backend/pnpm-workspace.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Backend/pnpm-workspace.yaml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					onlyBuiltDependencies:
 | 
				
			||||||
 | 
					  - '@biomejs/biome'
 | 
				
			||||||
 | 
					  - sharp
 | 
				
			||||||
							
								
								
									
										5
									
								
								Backend/postcss.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Backend/postcss.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
						plugins: {
 | 
				
			||||||
 | 
							"@tailwindcss/postcss": {},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								Backend/public/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Backend/public/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 23 KiB  | 
							
								
								
									
										127
									
								
								Backend/src/app/api/data/route.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								Backend/src/app/api/data/route.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,127 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pool from "@/utils/db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function GET() {
 | 
				
			||||||
 | 
					  const runResults = await pool.query<{
 | 
				
			||||||
 | 
					    run_id: number;
 | 
				
			||||||
 | 
					    run_name: string;
 | 
				
			||||||
 | 
					    run_date: string;
 | 
				
			||||||
 | 
					    totalseconds: string;
 | 
				
			||||||
 | 
					  }>(`
 | 
				
			||||||
 | 
					        SELECT
 | 
				
			||||||
 | 
					          r.id as run_id, r.name as run_name, 
 | 
				
			||||||
 | 
					          r.created_at as run_date, SUM(td.totalSeconds) as totalSeconds
 | 
				
			||||||
 | 
					        FROM runs r
 | 
				
			||||||
 | 
					        LEFT JOIN test_durations td ON td.run_id = r.id
 | 
				
			||||||
 | 
					        GROUP BY r.id, r.name
 | 
				
			||||||
 | 
					        ORDER BY r.id
 | 
				
			||||||
 | 
					      `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get test results for each run
 | 
				
			||||||
 | 
					  // Get start times for each test
 | 
				
			||||||
 | 
					  const startTimes = await pool.query<{
 | 
				
			||||||
 | 
					    run_id: number;
 | 
				
			||||||
 | 
					    test_name: string;
 | 
				
			||||||
 | 
					    path: string;
 | 
				
			||||||
 | 
					    start_time: Date;
 | 
				
			||||||
 | 
					  }>(`
 | 
				
			||||||
 | 
					    SELECT 
 | 
				
			||||||
 | 
					      run_id,
 | 
				
			||||||
 | 
					      test_name,
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      created_at as start_time
 | 
				
			||||||
 | 
					    FROM test_runs
 | 
				
			||||||
 | 
					    WHERE run_type = 'start'
 | 
				
			||||||
 | 
					  `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get end times and results for each test
 | 
				
			||||||
 | 
					  const endResults = await pool.query<{
 | 
				
			||||||
 | 
					    run_id: number;
 | 
				
			||||||
 | 
					    test_name: string;
 | 
				
			||||||
 | 
					    path: string;
 | 
				
			||||||
 | 
					    end_time: Date;
 | 
				
			||||||
 | 
					    success: boolean;
 | 
				
			||||||
 | 
					    pending: boolean;
 | 
				
			||||||
 | 
					  }>(`
 | 
				
			||||||
 | 
					    SELECT 
 | 
				
			||||||
 | 
					      run_id,
 | 
				
			||||||
 | 
					      test_name,
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      created_at as end_time,
 | 
				
			||||||
 | 
					      success,
 | 
				
			||||||
 | 
					      COALESCE(pending, false) as pending
 | 
				
			||||||
 | 
					    FROM test_runs
 | 
				
			||||||
 | 
					    WHERE run_type = 'end'
 | 
				
			||||||
 | 
					  `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Combine the results
 | 
				
			||||||
 | 
					  const testResults = {
 | 
				
			||||||
 | 
					    rows: endResults.rows.map(end => {
 | 
				
			||||||
 | 
					      const start = startTimes.rows.find(
 | 
				
			||||||
 | 
					        start => 
 | 
				
			||||||
 | 
					          start.run_id === end.run_id && 
 | 
				
			||||||
 | 
					          start.test_name === end.test_name && 
 | 
				
			||||||
 | 
					          start.path === end.path
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!start) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        run_id: end.run_id,
 | 
				
			||||||
 | 
					        test_name: end.test_name,
 | 
				
			||||||
 | 
					        path: end.path,
 | 
				
			||||||
 | 
					        totalseconds: (end.end_time.getTime() - start.start_time.getTime()) / 1000,
 | 
				
			||||||
 | 
					        success: end.success,
 | 
				
			||||||
 | 
					        pending: end.pending
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }).filter(Boolean)
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Add unfinished tests
 | 
				
			||||||
 | 
					  const unfinishedTests = startTimes.rows.filter(start => 
 | 
				
			||||||
 | 
					    !endResults.rows.some(end => 
 | 
				
			||||||
 | 
					      end.run_id === start.run_id && 
 | 
				
			||||||
 | 
					      end.test_name === start.test_name && 
 | 
				
			||||||
 | 
					      end.path === start.path
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  ).map(start => ({
 | 
				
			||||||
 | 
					    run_id: start.run_id,
 | 
				
			||||||
 | 
					    test_name: start.test_name,
 | 
				
			||||||
 | 
					    path: start.path,
 | 
				
			||||||
 | 
					    totalseconds: 0, // Test hasn't finished yet
 | 
				
			||||||
 | 
					    success: false,
 | 
				
			||||||
 | 
					    pending: true
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  testResults.rows.push(...unfinishedTests);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Combine and format results
 | 
				
			||||||
 | 
					  const formatted = runResults.rows.map((run) => {
 | 
				
			||||||
 | 
					    const runTests = testResults.rows.filter(
 | 
				
			||||||
 | 
					      (test): test is NonNullable<typeof test> => test !== null && test.run_id === run.run_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const totalDurationSeconds = Number(run.totalseconds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      run_id: run.run_id,
 | 
				
			||||||
 | 
					      run_name: run.run_name,
 | 
				
			||||||
 | 
					      date: run.run_date,
 | 
				
			||||||
 | 
					      totalseconds: run.totalseconds,
 | 
				
			||||||
 | 
					      success_count: runTests.filter((test) => test.success).length,
 | 
				
			||||||
 | 
					      failure_count: runTests.filter((test) => !test.success).length,
 | 
				
			||||||
 | 
					      children: runTests.map((test) => ({
 | 
				
			||||||
 | 
					        test_name: test.test_name,
 | 
				
			||||||
 | 
					        path: test.path,
 | 
				
			||||||
 | 
					        totalseconds: test.totalseconds,
 | 
				
			||||||
 | 
					        percentage_of_total: Number(
 | 
				
			||||||
 | 
					          ((test.totalseconds / totalDurationSeconds) * 100).toFixed(2)
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        status: test.success ? "success" : "failure",
 | 
				
			||||||
 | 
					        pending: test.pending,
 | 
				
			||||||
 | 
					      })),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return Response.json(formatted);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										64
									
								
								Backend/src/app/api/run/create/route.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Backend/src/app/api/run/create/route.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import pool from "@/utils/db";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(request: Request) {
 | 
				
			||||||
 | 
					  let runId;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    let {
 | 
				
			||||||
 | 
					      runName,
 | 
				
			||||||
 | 
					      testName,
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      success,
 | 
				
			||||||
 | 
					      runId: passedRunId,
 | 
				
			||||||
 | 
					      runType = "start"
 | 
				
			||||||
 | 
					    } = await request.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pending;
 | 
				
			||||||
 | 
					    if (!passedRunId) {
 | 
				
			||||||
 | 
					      const res = await pool
 | 
				
			||||||
 | 
					        .query(
 | 
				
			||||||
 | 
					          `WITH new_run AS (
 | 
				
			||||||
 | 
					          INSERT INTO runs (name)
 | 
				
			||||||
 | 
					          VALUES ($1)
 | 
				
			||||||
 | 
					          RETURNING id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        SELECT id FROM new_run;`,
 | 
				
			||||||
 | 
					          [runName]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .then((r) => r.rows[0].id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return NextResponse.json({runId: res})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // create as Pending by default, then update and add the duration from the passed information
 | 
				
			||||||
 | 
					    // can likely remove runType as a whole? 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (runType === "end") pending = false;
 | 
				
			||||||
 | 
					    else pending = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Now create the test run entry with 'pending' status
 | 
				
			||||||
 | 
					    if (passedRunId) {
 | 
				
			||||||
 | 
					      await pool.query(
 | 
				
			||||||
 | 
					        `INSERT INTO test_runs (run_id, test_name, path, run_type, created_at, success, pending)
 | 
				
			||||||
 | 
					         VALUES ($1, $2, $3, $4, NOW(), $5, $6);`,
 | 
				
			||||||
 | 
					        [passedRunId, testName, path, runType, success, pending]
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return NextResponse.json({ runId });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error creating run:", error);
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: "Failed to create run",
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        message: error.message,
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        detail: error.detail,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { status: 500 }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								Backend/src/app/layout.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								Backend/src/app/layout.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import "@/styles/globals.css";
 | 
				
			||||||
 | 
					import type { Metadata } from "next";
 | 
				
			||||||
 | 
					import { Geist } from "next/font/google";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  focusManager,
 | 
				
			||||||
 | 
					  QueryClient,
 | 
				
			||||||
 | 
					  QueryClientProvider,
 | 
				
			||||||
 | 
					} from "@tanstack/react-query";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const queryClient = new QueryClient({
 | 
				
			||||||
 | 
					  defaultOptions: {
 | 
				
			||||||
 | 
					    queries: {
 | 
				
			||||||
 | 
					      refetchOnWindowFocus: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const geist = Geist({
 | 
				
			||||||
 | 
					  subsets: ["latin"],
 | 
				
			||||||
 | 
					  variable: "--font-geist-sans",
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function RootLayout({
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					}: Readonly<{ children: React.ReactNode }>) {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    focusManager.setEventListener((handleFocus) => {
 | 
				
			||||||
 | 
					      const focus = () => {
 | 
				
			||||||
 | 
					        if (!document.hidden) {
 | 
				
			||||||
 | 
					          // console.log("Focus from parent");
 | 
				
			||||||
 | 
					          handleFocus(true);
 | 
				
			||||||
 | 
					          queryClient.refetchQueries(); // Without this, the above 'focus from parent' will be logged but queries will only refetch the first time the window is refocused from a different program. After that, data will not be refetched.
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      window.addEventListener("visibilitychange", focus);
 | 
				
			||||||
 | 
					      window.addEventListener("focus", focus);
 | 
				
			||||||
 | 
					      return () => {
 | 
				
			||||||
 | 
					        window.removeEventListener("visibilitychange", focus);
 | 
				
			||||||
 | 
					        window.removeEventListener("focus", focus);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <html lang="en" className={`${geist.variable}`}>
 | 
				
			||||||
 | 
					      <body>
 | 
				
			||||||
 | 
					        <QueryClientProvider client={queryClient}>
 | 
				
			||||||
 | 
					          {children}
 | 
				
			||||||
 | 
					        </QueryClientProvider>
 | 
				
			||||||
 | 
					      </body>
 | 
				
			||||||
 | 
					    </html>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								Backend/src/app/page.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Backend/src/app/page.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FoldableList } from "@/components/foldable-list";
 | 
				
			||||||
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
 | 
					import { useQuery } from "@tanstack/react-query";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fetcher = (url: string) => {
 | 
				
			||||||
 | 
					  return fetch(url).then((r) => r.json());
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update the page component to use full width
 | 
				
			||||||
 | 
					export default function Home() {
 | 
				
			||||||
 | 
					  const { isPending, error, data, isError } = useQuery({
 | 
				
			||||||
 | 
					    queryKey: ["historicData"],
 | 
				
			||||||
 | 
					    queryFn: () => fetcher(`${window.location}/api/data`),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <main className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-800 text-white">
 | 
				
			||||||
 | 
					      <div className="w-full max-w-4xl mx-auto">
 | 
				
			||||||
 | 
					        <h1 className="text-2xl font-semibold mb-6 text-center">
 | 
				
			||||||
 | 
					          {/* Corporate Planning AG - Historic Test Data */}
 | 
				
			||||||
 | 
					          Historic Test Data
 | 
				
			||||||
 | 
					        </h1>
 | 
				
			||||||
 | 
					        <div className="">
 | 
				
			||||||
 | 
					          {isError && (
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <h1>Encountered an Error while running the Query.</h1>
 | 
				
			||||||
 | 
					              <code>{error.message}</code>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {(!isPending && <FoldableList results={data as Result[]} />) || (
 | 
				
			||||||
 | 
					            <h1 className="text-2xl font-semibold mb-6 text-center">
 | 
				
			||||||
 | 
					              Loading...
 | 
				
			||||||
 | 
					            </h1>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										133
									
								
								Backend/src/components/date-group.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								Backend/src/components/date-group.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useMemo } from "react";
 | 
				
			||||||
 | 
					import { ChevronDown, Download } from "lucide-react";
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Collapsible,
 | 
				
			||||||
 | 
					  CollapsibleContent,
 | 
				
			||||||
 | 
					  CollapsibleTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/collapsible";
 | 
				
			||||||
 | 
					import { Button } from "./ui/button";
 | 
				
			||||||
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
 | 
					import toCSV from "@/utils/toCSV";
 | 
				
			||||||
 | 
					import { RunItem } from "./run-item";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DateGroupProps {
 | 
				
			||||||
 | 
					  dateGroup: {
 | 
				
			||||||
 | 
					    date: string;
 | 
				
			||||||
 | 
					    runs: Result[];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
				
			||||||
 | 
					  const [isOpen, setIsOpen] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Calculate aggregate statistics for this date
 | 
				
			||||||
 | 
					  const stats = useMemo(() => {
 | 
				
			||||||
 | 
					    let totalSuccess = 0;
 | 
				
			||||||
 | 
					    let totalFailure = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dateGroup.runs.forEach((run) => {
 | 
				
			||||||
 | 
					      totalSuccess += run.success_count;
 | 
				
			||||||
 | 
					      totalFailure += run.failure_count;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const totalTests = totalSuccess + totalFailure;
 | 
				
			||||||
 | 
					    const successPercentage =
 | 
				
			||||||
 | 
					      totalTests > 0 ? (totalSuccess / totalTests) * 100 : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      totalSuccess,
 | 
				
			||||||
 | 
					      totalFailure,
 | 
				
			||||||
 | 
					      totalTests,
 | 
				
			||||||
 | 
					      successPercentage,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [dateGroup]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleExportCSV = () => {
 | 
				
			||||||
 | 
					    // Create CSV header
 | 
				
			||||||
 | 
					    const headers = [
 | 
				
			||||||
 | 
					      "Run Name",
 | 
				
			||||||
 | 
					      "Test Name",
 | 
				
			||||||
 | 
					      "Path",
 | 
				
			||||||
 | 
					      "Status",
 | 
				
			||||||
 | 
					      "Duration (s)",
 | 
				
			||||||
 | 
					      "Percentage of Total",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create and trigger download
 | 
				
			||||||
 | 
					    const blob = new Blob([...toCSV(headers, dateGroup)], {
 | 
				
			||||||
 | 
					      type: "text/csv;charset=utf-8;",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const link = document.createElement("a");
 | 
				
			||||||
 | 
					    const url = URL.createObjectURL(blob);
 | 
				
			||||||
 | 
					    link.setAttribute("href", url);
 | 
				
			||||||
 | 
					    link.setAttribute(
 | 
				
			||||||
 | 
					      "download",
 | 
				
			||||||
 | 
					      `test-results-${new Date(dateGroup.date)
 | 
				
			||||||
 | 
					        .toISOString()
 | 
				
			||||||
 | 
					        .substring(0, 10)}.csv`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    document.body.appendChild(link);
 | 
				
			||||||
 | 
					    link.click();
 | 
				
			||||||
 | 
					    document.body.removeChild(link);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="mb-4 border border-zinc-600 rounded-lg overflow-hidden">
 | 
				
			||||||
 | 
					      <Collapsible
 | 
				
			||||||
 | 
					        defaultOpen={true}
 | 
				
			||||||
 | 
					        open={isOpen}
 | 
				
			||||||
 | 
					        onOpenChange={setIsOpen}
 | 
				
			||||||
 | 
					        className="w-full"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <CollapsibleTrigger className="flex w-full items-center justify-between py-4 text-left font-medium  hover:bg-zinc-600  focus-visible:ring-offset-2 px-4  transition-colors bg-zinc-700">
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-4">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="default"
 | 
				
			||||||
 | 
					              size="sm"
 | 
				
			||||||
 | 
					              onClick={(e: React.MouseEvent) => {
 | 
				
			||||||
 | 
					                e.stopPropagation();
 | 
				
			||||||
 | 
					                handleExportCSV();
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              className="flex items-center gap-2"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <Download className="h-4 w-4" />
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            <span className="text-lg font-semibold">{dateGroup.date}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-4">
 | 
				
			||||||
 | 
					            <div className="flex items-center">
 | 
				
			||||||
 | 
					              <div className="rounded-full flex gap-1 pr-1"></div>
 | 
				
			||||||
 | 
					              <span className="text-xs text-zinc-400 ml-2">
 | 
				
			||||||
 | 
					                {stats.totalSuccess}/{stats.totalTests} passed
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <span className="text-sm text-zinc-200">
 | 
				
			||||||
 | 
					              {dateGroup.runs.length} runs
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					            <ChevronDown
 | 
				
			||||||
 | 
					              className={cn(
 | 
				
			||||||
 | 
					                "h-4 w-4 text-zinc-100 transition-transform duration-200",
 | 
				
			||||||
 | 
					                isOpen && "rotate-180"
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CollapsibleTrigger>
 | 
				
			||||||
 | 
					        <CollapsibleContent>
 | 
				
			||||||
 | 
					          <div className="divide-y divide-zinc-700">
 | 
				
			||||||
 | 
					            {dateGroup.runs
 | 
				
			||||||
 | 
					              .sort(
 | 
				
			||||||
 | 
					                (a, b) =>
 | 
				
			||||||
 | 
					                  new Date(b.date).getTime() - new Date(a.date).getTime()
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					              .map((run) => (
 | 
				
			||||||
 | 
					                <RunItem key={run.run_id} result={run} />
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CollapsibleContent>
 | 
				
			||||||
 | 
					      </Collapsible>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								Backend/src/components/foldable-list.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								Backend/src/components/foldable-list.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useMemo } from "react";
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
 | 
					import { DateGroup } from "./date-group";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Type for date-grouped results
 | 
				
			||||||
 | 
					type DateGroupedResults = {
 | 
				
			||||||
 | 
					  [date: string]: {
 | 
				
			||||||
 | 
					    date: string;
 | 
				
			||||||
 | 
					    runs: Result[];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FoldableListProps {
 | 
				
			||||||
 | 
					  results: Result[];
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function FoldableList({ results, className }: FoldableListProps) {
 | 
				
			||||||
 | 
					  if (!results[0]?.run_id)
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <h1 className="text-center text-red-400">
 | 
				
			||||||
 | 
					        There was an Error while fetching test results.
 | 
				
			||||||
 | 
					      </h1>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  // Group results by date
 | 
				
			||||||
 | 
					  const dateGroupedResults = useMemo(() => {
 | 
				
			||||||
 | 
					    const grouped: DateGroupedResults = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    results.forEach((result) => {
 | 
				
			||||||
 | 
					      const date = new Date(result.date).toDateString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!grouped[date]) {
 | 
				
			||||||
 | 
					        grouped[date] = {
 | 
				
			||||||
 | 
					          date,
 | 
				
			||||||
 | 
					          runs: [],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      grouped[date].runs.push(result);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert to array and sort by date (newest first)
 | 
				
			||||||
 | 
					    return Object.values(grouped).sort(
 | 
				
			||||||
 | 
					      (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, [results]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={cn("w-full", className)}>
 | 
				
			||||||
 | 
					      {dateGroupedResults.map((dateGroup) => (
 | 
				
			||||||
 | 
					        <DateGroup key={dateGroup.date} dateGroup={dateGroup} />
 | 
				
			||||||
 | 
					      ))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										114
									
								
								Backend/src/components/run-item.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								Backend/src/components/run-item.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,114 @@
 | 
				
			||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { ChevronDown } from "lucide-react";
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Collapsible,
 | 
				
			||||||
 | 
					  CollapsibleContent,
 | 
				
			||||||
 | 
					  CollapsibleTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/collapsible";
 | 
				
			||||||
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RunItemProps {
 | 
				
			||||||
 | 
					  result: Result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RunItem({ result }: RunItemProps) {
 | 
				
			||||||
 | 
					  const [isOpen, setIsOpen] = useState(false);
 | 
				
			||||||
 | 
					  const hasChildren = result.children && result.children.length > 0;
 | 
				
			||||||
 | 
					  const totalTests = result.success_count + result.failure_count;
 | 
				
			||||||
 | 
					  const successPercentage =
 | 
				
			||||||
 | 
					    totalTests > 0 ? (result.success_count / totalTests) * 100 : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const formatSeconds = (seconds: number | string): string => {
 | 
				
			||||||
 | 
					    var hours: number | string = Math.floor((seconds as number) / 3600);
 | 
				
			||||||
 | 
					    var minutes: number | string = Math.floor(
 | 
				
			||||||
 | 
					      ((seconds as number) - hours * 3600) / 60
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    seconds = (seconds as number) - hours * 3600 - minutes * 60;
 | 
				
			||||||
 | 
					    if (hours < 10) {
 | 
				
			||||||
 | 
					      hours = "0" + hours;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (minutes < 10) {
 | 
				
			||||||
 | 
					      minutes = "0" + minutes;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (seconds < 10) {
 | 
				
			||||||
 | 
					      seconds = "0" + seconds.toFixed(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var time = hours + ":" + minutes + ":" + seconds;
 | 
				
			||||||
 | 
					    return time;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="w-full">
 | 
				
			||||||
 | 
					      <Collapsible
 | 
				
			||||||
 | 
					        defaultOpen={false}
 | 
				
			||||||
 | 
					        open={isOpen}
 | 
				
			||||||
 | 
					        onOpenChange={setIsOpen}
 | 
				
			||||||
 | 
					        className="w-full"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <CollapsibleTrigger className="flex w-full items-center justify-between py-3 text-left font-medium text-zinc-300 hover:bg-zinc-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2 px-4  transition-colors">
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					            <span>{result.run_name}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="flex items-center gap-4">
 | 
				
			||||||
 | 
					            <div className="flex items-center">
 | 
				
			||||||
 | 
					              <div className="w-24 h-1.5 rounded-full overflow-hidden flex">
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className="h-full bg-green-500"
 | 
				
			||||||
 | 
					                  style={{ width: `${successPercentage}%` }}
 | 
				
			||||||
 | 
					                ></div>
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className="h-full bg-red-500"
 | 
				
			||||||
 | 
					                  style={{ width: `${100 - successPercentage}%` }}
 | 
				
			||||||
 | 
					                ></div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <span className="text-xs text-zinc-400 ml-2">
 | 
				
			||||||
 | 
					                {result.success_count}/{totalTests} passed
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <span className="text-sm text-zinc-300">
 | 
				
			||||||
 | 
					              {formatSeconds(result.totalseconds)}
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					            <ChevronDown
 | 
				
			||||||
 | 
					              className={cn(
 | 
				
			||||||
 | 
					                "h-4 w-4 text-gray-500 transition-transform duration-200",
 | 
				
			||||||
 | 
					                isOpen && "rotate-180"
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </CollapsibleTrigger>
 | 
				
			||||||
 | 
					        <CollapsibleContent>
 | 
				
			||||||
 | 
					          {hasChildren &&
 | 
				
			||||||
 | 
					            result.children.map((child, index) => (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                key={index}
 | 
				
			||||||
 | 
					                className="flex items-center justify-between py-3 px-4 pl-8 border-t border-zinc-700 hover:bg-zinc-700"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    className={cn(
 | 
				
			||||||
 | 
					                      "w-2 h-2 rounded-full",
 | 
				
			||||||
 | 
					                      child.pending
 | 
				
			||||||
 | 
					                        ? "bg-blue-500"
 | 
				
			||||||
 | 
					                        : child.status === "success"
 | 
				
			||||||
 | 
					                        ? "bg-green-500"
 | 
				
			||||||
 | 
					                        : "bg-red-500"
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  ></div>
 | 
				
			||||||
 | 
					                  <span className="text-zinc-200">{child.test_name}</span>
 | 
				
			||||||
 | 
					                  <span className="text-xs text-zinc-400">({child.path})</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className="flex items-center gap-4 pr-8">
 | 
				
			||||||
 | 
					                  <span className="text-sm text-zinc-300 w-12 text-right">
 | 
				
			||||||
 | 
					                    {formatSeconds(child.totalseconds)}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					        </CollapsibleContent>
 | 
				
			||||||
 | 
					      </Collapsible>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								Backend/src/components/ui/badge.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Backend/src/components/ui/badge.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { Slot } from "@radix-ui/react-slot"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const badgeVariants = cva(
 | 
				
			||||||
 | 
					  "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
 | 
				
			||||||
 | 
					        secondary:
 | 
				
			||||||
 | 
					          "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
 | 
				
			||||||
 | 
					        outline:
 | 
				
			||||||
 | 
					          "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Badge({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  variant,
 | 
				
			||||||
 | 
					  asChild = false,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.ComponentProps<"span"> &
 | 
				
			||||||
 | 
					  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
 | 
				
			||||||
 | 
					  const Comp = asChild ? Slot : "span"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Comp
 | 
				
			||||||
 | 
					      data-slot="badge"
 | 
				
			||||||
 | 
					      className={cn(badgeVariants({ variant }), className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Badge, badgeVariants }
 | 
				
			||||||
							
								
								
									
										57
									
								
								Backend/src/components/ui/button.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Backend/src/components/ui/button.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					import { Slot } from "@radix-ui/react-slot";
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const buttonVariants = cva(
 | 
				
			||||||
 | 
					  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
 | 
				
			||||||
 | 
					        outline:
 | 
				
			||||||
 | 
					          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
 | 
				
			||||||
 | 
					        secondary:
 | 
				
			||||||
 | 
					          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
 | 
				
			||||||
 | 
					        ghost:
 | 
				
			||||||
 | 
					          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
 | 
				
			||||||
 | 
					        link: "text-primary underline-offset-4 hover:underline",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      size: {
 | 
				
			||||||
 | 
					        default: "h-9 px-4 py-2 has-[>svg]:px-3",
 | 
				
			||||||
 | 
					        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
 | 
				
			||||||
 | 
					        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
 | 
				
			||||||
 | 
					        icon: "size-9",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					      size: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Button({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  variant,
 | 
				
			||||||
 | 
					  size,
 | 
				
			||||||
 | 
					  asChild = false,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.ComponentProps<"button"> &
 | 
				
			||||||
 | 
					  VariantProps<typeof buttonVariants> & {
 | 
				
			||||||
 | 
					    asChild?: boolean;
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <span
 | 
				
			||||||
 | 
					      data-slot="button"
 | 
				
			||||||
 | 
					      className={cn(buttonVariants({ variant, size, className }))}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Button, buttonVariants };
 | 
				
			||||||
							
								
								
									
										11
									
								
								Backend/src/components/ui/collapsible.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Backend/src/components/ui/collapsible.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Collapsible = CollapsiblePrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Collapsible, CollapsibleTrigger, CollapsibleContent }
 | 
				
			||||||
							
								
								
									
										42
									
								
								Backend/src/env.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Backend/src/env.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					import { createEnv } from "@t3-oss/env-nextjs";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const env = createEnv({
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Specify your server-side environment variables schema here. This way you can ensure the app
 | 
				
			||||||
 | 
					   * isn't built with invalid env vars.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    NODE_ENV: z.enum(["development", "test", "production"]),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Specify your client-side environment variables schema here. This way you can ensure the app
 | 
				
			||||||
 | 
					   * isn't built with invalid env vars. To expose them to the client, prefix them with
 | 
				
			||||||
 | 
					   * `NEXT_PUBLIC_`.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  client: {
 | 
				
			||||||
 | 
					    NEXT_PUBLIC_DATABASE_URL: z.string()//.url(),
 | 
				
			||||||
 | 
					    // NEXT_PUBLIC_CLIENTVAR: z.string(),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
 | 
				
			||||||
 | 
					   * middlewares) or client-side so we need to destruct manually.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  runtimeEnv: {
 | 
				
			||||||
 | 
					    NODE_ENV: process.env.NODE_ENV,
 | 
				
			||||||
 | 
					    NEXT_PUBLIC_DATABASE_URL: process.env.NEXT_PUBLIC_DATABASE_URL,
 | 
				
			||||||
 | 
					    // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
 | 
				
			||||||
 | 
					   * useful for Docker builds.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
 | 
				
			||||||
 | 
					   * `SOME_VAR=''` will throw an error.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  emptyStringAsUndefined: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										16
									
								
								Backend/src/lib/types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Backend/src/lib/types.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					export type Result = {
 | 
				
			||||||
 | 
					  run_id: number;
 | 
				
			||||||
 | 
					  run_name: string;
 | 
				
			||||||
 | 
					  date: string;
 | 
				
			||||||
 | 
					  totalseconds: number;
 | 
				
			||||||
 | 
					  success_count: number;
 | 
				
			||||||
 | 
					  failure_count: number;
 | 
				
			||||||
 | 
					  children: Array<{
 | 
				
			||||||
 | 
					    test_name: string;
 | 
				
			||||||
 | 
					    path: string;
 | 
				
			||||||
 | 
					    totalseconds: number;
 | 
				
			||||||
 | 
					    percentage_of_total: number;
 | 
				
			||||||
 | 
					    status: "success" | "failure";
 | 
				
			||||||
 | 
					    pending: boolean
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										8
									
								
								Backend/src/lib/utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Backend/src/lib/utils.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { clsx, type ClassValue } from "clsx";
 | 
				
			||||||
 | 
					import { twMerge } from "tailwind-merge";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function cn(...inputs: ClassValue[]) {
 | 
				
			||||||
 | 
					  return twMerge(clsx(inputs));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function format(inputs: any) {}
 | 
				
			||||||
							
								
								
									
										125
									
								
								Backend/src/styles/globals.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								Backend/src/styles/globals.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,125 @@
 | 
				
			||||||
 | 
					@import "tailwindcss";
 | 
				
			||||||
 | 
					@import "tw-animate-css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@custom-variant dark (&:is(.dark *));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@theme {
 | 
				
			||||||
 | 
						--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
 | 
				
			||||||
 | 
							"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@theme inline {
 | 
				
			||||||
 | 
						--radius-sm: calc(var(--radius) - 4px);
 | 
				
			||||||
 | 
						--radius-md: calc(var(--radius) - 2px);
 | 
				
			||||||
 | 
						--radius-lg: var(--radius);
 | 
				
			||||||
 | 
						--radius-xl: calc(var(--radius) + 4px);
 | 
				
			||||||
 | 
						--color-background: var(--background);
 | 
				
			||||||
 | 
						--color-foreground: var(--foreground);
 | 
				
			||||||
 | 
						--color-card: var(--card);
 | 
				
			||||||
 | 
						--color-card-foreground: var(--card-foreground);
 | 
				
			||||||
 | 
						--color-popover: var(--popover);
 | 
				
			||||||
 | 
						--color-popover-foreground: var(--popover-foreground);
 | 
				
			||||||
 | 
						--color-primary: var(--primary);
 | 
				
			||||||
 | 
						--color-primary-foreground: var(--primary-foreground);
 | 
				
			||||||
 | 
						--color-secondary: var(--secondary);
 | 
				
			||||||
 | 
						--color-secondary-foreground: var(--secondary-foreground);
 | 
				
			||||||
 | 
						--color-muted: var(--muted);
 | 
				
			||||||
 | 
						--color-muted-foreground: var(--muted-foreground);
 | 
				
			||||||
 | 
						--color-accent: var(--accent);
 | 
				
			||||||
 | 
						--color-accent-foreground: var(--accent-foreground);
 | 
				
			||||||
 | 
						--color-destructive: var(--destructive);
 | 
				
			||||||
 | 
						--color-border: var(--border);
 | 
				
			||||||
 | 
						--color-input: var(--input);
 | 
				
			||||||
 | 
						--color-ring: var(--ring);
 | 
				
			||||||
 | 
						--color-chart-1: var(--chart-1);
 | 
				
			||||||
 | 
						--color-chart-2: var(--chart-2);
 | 
				
			||||||
 | 
						--color-chart-3: var(--chart-3);
 | 
				
			||||||
 | 
						--color-chart-4: var(--chart-4);
 | 
				
			||||||
 | 
						--color-chart-5: var(--chart-5);
 | 
				
			||||||
 | 
						--color-sidebar: var(--sidebar);
 | 
				
			||||||
 | 
						--color-sidebar-foreground: var(--sidebar-foreground);
 | 
				
			||||||
 | 
						--color-sidebar-primary: var(--sidebar-primary);
 | 
				
			||||||
 | 
						--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
 | 
				
			||||||
 | 
						--color-sidebar-accent: var(--sidebar-accent);
 | 
				
			||||||
 | 
						--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
 | 
				
			||||||
 | 
						--color-sidebar-border: var(--sidebar-border);
 | 
				
			||||||
 | 
						--color-sidebar-ring: var(--sidebar-ring);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:root {
 | 
				
			||||||
 | 
						--radius: 0.625rem;
 | 
				
			||||||
 | 
						--background: oklch(1 0 0);
 | 
				
			||||||
 | 
						--foreground: oklch(0.141 0.005 285.823);
 | 
				
			||||||
 | 
						--card: oklch(1 0 0);
 | 
				
			||||||
 | 
						--card-foreground: oklch(0.141 0.005 285.823);
 | 
				
			||||||
 | 
						--popover: oklch(1 0 0);
 | 
				
			||||||
 | 
						--popover-foreground: oklch(0.141 0.005 285.823);
 | 
				
			||||||
 | 
						--primary: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--primary-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--secondary: oklch(0.967 0.001 286.375);
 | 
				
			||||||
 | 
						--secondary-foreground: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--muted: oklch(0.967 0.001 286.375);
 | 
				
			||||||
 | 
						--muted-foreground: oklch(0.552 0.016 285.938);
 | 
				
			||||||
 | 
						--accent: oklch(0.967 0.001 286.375);
 | 
				
			||||||
 | 
						--accent-foreground: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--destructive: oklch(0.577 0.245 27.325);
 | 
				
			||||||
 | 
						--border: oklch(0.92 0.004 286.32);
 | 
				
			||||||
 | 
						--input: oklch(0.92 0.004 286.32);
 | 
				
			||||||
 | 
						--ring: oklch(0.705 0.015 286.067);
 | 
				
			||||||
 | 
						--chart-1: oklch(0.646 0.222 41.116);
 | 
				
			||||||
 | 
						--chart-2: oklch(0.6 0.118 184.704);
 | 
				
			||||||
 | 
						--chart-3: oklch(0.398 0.07 227.392);
 | 
				
			||||||
 | 
						--chart-4: oklch(0.828 0.189 84.429);
 | 
				
			||||||
 | 
						--chart-5: oklch(0.769 0.188 70.08);
 | 
				
			||||||
 | 
						--sidebar: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--sidebar-foreground: oklch(0.141 0.005 285.823);
 | 
				
			||||||
 | 
						--sidebar-primary: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--sidebar-primary-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--sidebar-accent: oklch(0.967 0.001 286.375);
 | 
				
			||||||
 | 
						--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--sidebar-border: oklch(0.92 0.004 286.32);
 | 
				
			||||||
 | 
						--sidebar-ring: oklch(0.705 0.015 286.067);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dark {
 | 
				
			||||||
 | 
						--background: oklch(0.141 0.005 285.823);
 | 
				
			||||||
 | 
						--foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--card: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--card-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--popover: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--popover-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--primary: oklch(0.92 0.004 286.32);
 | 
				
			||||||
 | 
						--primary-foreground: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--secondary: oklch(0.274 0.006 286.033);
 | 
				
			||||||
 | 
						--secondary-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--muted: oklch(0.274 0.006 286.033);
 | 
				
			||||||
 | 
						--muted-foreground: oklch(0.705 0.015 286.067);
 | 
				
			||||||
 | 
						--accent: oklch(0.274 0.006 286.033);
 | 
				
			||||||
 | 
						--accent-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--destructive: oklch(0.704 0.191 22.216);
 | 
				
			||||||
 | 
						--border: oklch(1 0 0 / 10%);
 | 
				
			||||||
 | 
						--input: oklch(1 0 0 / 15%);
 | 
				
			||||||
 | 
						--ring: oklch(0.552 0.016 285.938);
 | 
				
			||||||
 | 
						--chart-1: oklch(0.488 0.243 264.376);
 | 
				
			||||||
 | 
						--chart-2: oklch(0.696 0.17 162.48);
 | 
				
			||||||
 | 
						--chart-3: oklch(0.769 0.188 70.08);
 | 
				
			||||||
 | 
						--chart-4: oklch(0.627 0.265 303.9);
 | 
				
			||||||
 | 
						--chart-5: oklch(0.645 0.246 16.439);
 | 
				
			||||||
 | 
						--sidebar: oklch(0.21 0.006 285.885);
 | 
				
			||||||
 | 
						--sidebar-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--sidebar-primary: oklch(0.488 0.243 264.376);
 | 
				
			||||||
 | 
						--sidebar-primary-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--sidebar-accent: oklch(0.274 0.006 286.033);
 | 
				
			||||||
 | 
						--sidebar-accent-foreground: oklch(0.985 0 0);
 | 
				
			||||||
 | 
						--sidebar-border: oklch(1 0 0 / 10%);
 | 
				
			||||||
 | 
						--sidebar-ring: oklch(0.552 0.016 285.938);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@layer base {
 | 
				
			||||||
 | 
					  * {
 | 
				
			||||||
 | 
					    @apply border-border outline-ring/50;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    @apply bg-background text-foreground;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								Backend/src/utils/db.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Backend/src/utils/db.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import "../env";
 | 
				
			||||||
 | 
					import { Pool } from "pg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// console.log(process.env)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pool = new Pool({
 | 
				
			||||||
 | 
					  connectionString: process.env.NEXT_PUBLIC_DATABASE_URL,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default pool;
 | 
				
			||||||
							
								
								
									
										19
									
								
								Backend/src/utils/toCSV.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Backend/src/utils/toCSV.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function toCSV(
 | 
				
			||||||
 | 
					  headers: string[],
 | 
				
			||||||
 | 
					  rows: { date: string; runs: Result[] }
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const a = rows.runs.flatMap((run) =>
 | 
				
			||||||
 | 
					    run.children.map((child) => [
 | 
				
			||||||
 | 
					      run.run_name,
 | 
				
			||||||
 | 
					      child.test_name,
 | 
				
			||||||
 | 
					      child.path,
 | 
				
			||||||
 | 
					      child.status,
 | 
				
			||||||
 | 
					      child.totalseconds,
 | 
				
			||||||
 | 
					      child.percentage_of_total,
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [headers.join(","), ...a.map((row) => row.join(","))].join("\n");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								Backend/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Backend/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"compilerOptions": {
 | 
				
			||||||
 | 
							/* Base Options: */
 | 
				
			||||||
 | 
							"esModuleInterop": true,
 | 
				
			||||||
 | 
							"skipLibCheck": true,
 | 
				
			||||||
 | 
							"target": "es2022",
 | 
				
			||||||
 | 
							"allowJs": true,
 | 
				
			||||||
 | 
							"resolveJsonModule": true,
 | 
				
			||||||
 | 
							"moduleDetection": "force",
 | 
				
			||||||
 | 
							"isolatedModules": true,
 | 
				
			||||||
 | 
							"verbatimModuleSyntax": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/* Strictness */
 | 
				
			||||||
 | 
							"strict": true,
 | 
				
			||||||
 | 
							"noUncheckedIndexedAccess": true,
 | 
				
			||||||
 | 
							"checkJs": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/* Bundled projects */
 | 
				
			||||||
 | 
							"lib": ["dom", "dom.iterable", "ES2022"],
 | 
				
			||||||
 | 
							"noEmit": true,
 | 
				
			||||||
 | 
							"module": "ESNext",
 | 
				
			||||||
 | 
							"moduleResolution": "Bundler",
 | 
				
			||||||
 | 
							"jsx": "preserve",
 | 
				
			||||||
 | 
							"plugins": [{ "name": "next" }],
 | 
				
			||||||
 | 
							"incremental": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/* Path Aliases */
 | 
				
			||||||
 | 
							"baseUrl": ".",
 | 
				
			||||||
 | 
							"paths": {
 | 
				
			||||||
 | 
								"@/*": ["./src/*"]
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"include": [
 | 
				
			||||||
 | 
							"next-env.d.ts",
 | 
				
			||||||
 | 
							"**/*.ts",
 | 
				
			||||||
 | 
							"**/*.tsx",
 | 
				
			||||||
 | 
							"**/*.cjs",
 | 
				
			||||||
 | 
							"**/*.js",
 | 
				
			||||||
 | 
							".next/types/**/*.ts"
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						"exclude": ["node_modules"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								Client/cypress.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Client/cypress.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					const { defineConfig } = require("cypress");
 | 
				
			||||||
 | 
					const apiRequestPlugin = require("./src/index");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = defineConfig({
 | 
				
			||||||
 | 
					  historyTrackerURL: "http://localhost:3001",
 | 
				
			||||||
 | 
					  e2e: {
 | 
				
			||||||
 | 
					    setupNodeEvents(on, config) {
 | 
				
			||||||
 | 
					      // Set up the API request plugin
 | 
				
			||||||
 | 
					      apiRequestPlugin(on, config);
 | 
				
			||||||
 | 
					      return config;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										143
									
								
								Client/cypress/e2e/1-getting-started/todo.cy.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								Client/cypress/e2e/1-getting-started/todo.cy.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										5
									
								
								Client/cypress/fixtures/example.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Client/cypress/fixtures/example.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "Using fixtures to represent data",
 | 
				
			||||||
 | 
					  "email": "hello@cypress.io",
 | 
				
			||||||
 | 
					  "body": "Fixtures are a great way to mock data for responses to routes"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										0
									
								
								Client/cypress/support/e2e.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Client/cypress/support/e2e.js
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										49
									
								
								Client/rollup.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Client/rollup.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,49 @@
 | 
				
			||||||
 | 
					import pkg from "./package.json";
 | 
				
			||||||
 | 
					import del from "rollup-plugin-delete";
 | 
				
			||||||
 | 
					import commonjs from "rollup-plugin-commonjs";
 | 
				
			||||||
 | 
					import json from "rollup-plugin-json";
 | 
				
			||||||
 | 
					import copy from "rollup-plugin-copy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const config = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    input: "./src/index.js",
 | 
				
			||||||
 | 
					    output: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        file: pkg.main,
 | 
				
			||||||
 | 
					        format: "cjs",
 | 
				
			||||||
 | 
					        globals: {
 | 
				
			||||||
 | 
					          Cypress: "cypress",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { file: pkg.module, format: "es" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    plugins: [
 | 
				
			||||||
 | 
					      // Delete contents of target folder
 | 
				
			||||||
 | 
					      del({
 | 
				
			||||||
 | 
					        targets: pkg.files,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Resolve JSON files
 | 
				
			||||||
 | 
					      json(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Compile to commonjs and bundle
 | 
				
			||||||
 | 
					      commonjs(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Copy type definitions to target folder
 | 
				
			||||||
 | 
					      // copy({
 | 
				
			||||||
 | 
					      //     targets: [{ src: './types/**/*.d.ts', dest: './dist/types' }],
 | 
				
			||||||
 | 
					      // }),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Mark all dependencies as external to prevent Rollup from
 | 
				
			||||||
 | 
					     * including them in the bundle. We'll let the package manager
 | 
				
			||||||
 | 
					     * take care of dependency resolution and stuff so we don't
 | 
				
			||||||
 | 
					     * have to download the exact same code multiple times, once
 | 
				
			||||||
 | 
					     * in this bundle and also as a dependency of another package.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    external: [...Object.keys(pkg.dependencies || {})],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = config;
 | 
				
			||||||
							
								
								
									
										135
									
								
								Client/src/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								Client/src/index.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,135 @@
 | 
				
			||||||
 | 
					// This plugin makes API requests at specific points during Cypress test execution
 | 
				
			||||||
 | 
					// - Before any test in the entire test suite runs
 | 
				
			||||||
 | 
					// - Before each test file runs
 | 
				
			||||||
 | 
					// - After each test file completes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Makes an API request with the provided configuration
 | 
				
			||||||
 | 
					 * @param {Object} options - Request options
 | 
				
			||||||
 | 
					 * @param {string} options.url - The URL to request
 | 
				
			||||||
 | 
					 * @param {string} options.method - HTTP method (GET, POST, etc.)
 | 
				
			||||||
 | 
					 * @param {Object} [options.body] - Request body for POST/PUT/PATCH requests
 | 
				
			||||||
 | 
					 * @param {Object} [options.headers] - Request headers
 | 
				
			||||||
 | 
					 * @param {string} eventType - Description of when this request is being made
 | 
				
			||||||
 | 
					 * @returns {Promise} - Promise resolving to the API response
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const makeApiRequest = async (options, eventType) => {
 | 
				
			||||||
 | 
					  console.log(`Making API request for event: ${eventType}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await fetch(options.url, {
 | 
				
			||||||
 | 
					      method: options.method || "GET",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					        ...options.headers,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      ...(options.body && { body: JSON.stringify(options.body) }),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await response.json();
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      `API request for ${eventType} completed with status: ${response.status}`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    console.log(data);
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error(`API request for ${eventType} failed:`, error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Configuration for the different API requests to be made
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const apiRequests = {
 | 
				
			||||||
 | 
					  beforeAllTests: {},
 | 
				
			||||||
 | 
					  beforeTestFile: {},
 | 
				
			||||||
 | 
					  afterTestFile: {},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Sets up the plugin in Cypress
 | 
				
			||||||
 | 
					 * @param {Object} on - Cypress event registration function
 | 
				
			||||||
 | 
					 * @param {Object} config - Cypress configuration
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					// Track if event handlers have been registered to prevent duplicate registrations
 | 
				
			||||||
 | 
					let eventHandlersRegistered = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setupApiRequestPlugin(on, config) {
 | 
				
			||||||
 | 
					  // If event handlers are already registered, don't register them again
 | 
				
			||||||
 | 
					  if (eventHandlersRegistered) {
 | 
				
			||||||
 | 
					    console.log("API plugin event handlers already registered, skipping...");
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Track whether we've already run the before all tests request
 | 
				
			||||||
 | 
					  let beforeAllTestsExecuted = false;
 | 
				
			||||||
 | 
					  // Track the current run ID across all tests
 | 
				
			||||||
 | 
					  let currentRunId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Before all tests run (only once)
 | 
				
			||||||
 | 
					  on("before:run", async () => {
 | 
				
			||||||
 | 
					    if (!beforeAllTestsExecuted) {
 | 
				
			||||||
 | 
					      apiRequests.beforeAllTests = {
 | 
				
			||||||
 | 
					        url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: {
 | 
				
			||||||
 | 
					          runName: `Cypress Run @ ${new Date().toLocaleTimeString()}`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const res = await makeApiRequest(
 | 
				
			||||||
 | 
					        apiRequests.beforeAllTests,
 | 
				
			||||||
 | 
					        "beforeAllTests",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      currentRunId = res.runId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      beforeAllTestsExecuted = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO: FIGURE OUT WHY THIS RUNS TWICE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Process individual test events
 | 
				
			||||||
 | 
					  on("before:spec", async (test) => {
 | 
				
			||||||
 | 
					    if (!currentRunId) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testRequest = {
 | 
				
			||||||
 | 
					      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      body: {
 | 
				
			||||||
 | 
					        runId: currentRunId,
 | 
				
			||||||
 | 
					        testName: test.name,
 | 
				
			||||||
 | 
					        path: test.relative,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await makeApiRequest(testRequest, "beforeTest");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  on("after:spec", async (test, results) => {
 | 
				
			||||||
 | 
					    if (!currentRunId) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testRequest = {
 | 
				
			||||||
 | 
					      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      body: {
 | 
				
			||||||
 | 
					        runId: currentRunId,
 | 
				
			||||||
 | 
					        testName: test.name,
 | 
				
			||||||
 | 
					        path: test.relative,
 | 
				
			||||||
 | 
					        runType: "end",
 | 
				
			||||||
 | 
					        success: results.stats.failures === 0,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("specs", results);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await makeApiRequest(testRequest, "afterTest");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Mark event handlers as registered to prevent duplicate registrations
 | 
				
			||||||
 | 
					  eventHandlersRegistered = true;
 | 
				
			||||||
 | 
					  console.log("API plugin event handlers registered successfully");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = setupApiRequestPlugin;
 | 
				
			||||||
							
								
								
									
										9
									
								
								Client/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Client/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "lib": ["es2015", "dom"],
 | 
				
			||||||
 | 
					    "allowJs": true,
 | 
				
			||||||
 | 
					    "types": ["cypress"],
 | 
				
			||||||
 | 
					    "outDir": "./dist"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["cypress/**/*.js", "src/**/*.js"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								compose.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								compose.yml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					version: "3.8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  db:
 | 
				
			||||||
 | 
					    image: postgres:latest
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./docker/db-database:/var/lib/postgresql/data
 | 
				
			||||||
 | 
					      - ./docker/entrypoint:/docker-entrypoint-initdb.d
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      POSTGRES_DB: schoolproject
 | 
				
			||||||
 | 
					      POSTGRES_USER: postgres
 | 
				
			||||||
 | 
					      POSTGRES_PASSWORD: postgres
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "5432:5432"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # backend:
 | 
				
			||||||
 | 
					  #   build:
 | 
				
			||||||
 | 
					  #     context: ./Backend
 | 
				
			||||||
 | 
					  #     dockerfile: Dockerfile
 | 
				
			||||||
							
								
								
									
										14
									
								
								docker/ConnectionTests/01.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								docker/ConnectionTests/01.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    -- *,
 | 
				
			||||||
 | 
					    EXTRACT(EPOCH FROM age(tr_end.created_at, tr_start.created_at)) as totalSec,
 | 
				
			||||||
 | 
					    tr_end.created_at - tr_start.created_at as duration
 | 
				
			||||||
 | 
					FROM test_runs tr_end
 | 
				
			||||||
 | 
					    JOIN test_runs tr_start 
 | 
				
			||||||
 | 
					        ON tr_start.run_id = tr_end.run_id 
 | 
				
			||||||
 | 
					        AND tr_start.test_name = tr_end.test_name
 | 
				
			||||||
 | 
					        AND tr_start.path = tr_end.path
 | 
				
			||||||
 | 
					        AND tr_start.run_type = 'start'
 | 
				
			||||||
 | 
					        AND tr_end.run_type = 'end';
 | 
				
			||||||
 | 
					    -- LEFT JOIN runs r on td.run_id = r.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SELECT * from test_durations;
 | 
				
			||||||
							
								
								
									
										32
									
								
								docker/entrypoint/01-init.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								docker/entrypoint/01-init.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					-- Drop existing tables if they exist
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS test_runs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS runs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Drop existing enum if it exists
 | 
				
			||||||
 | 
					DROP TYPE IF EXISTS run_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Create an ENUM type for RunType
 | 
				
			||||||
 | 
					CREATE TYPE run_type AS ENUM ('start', 'end');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Create Runs table
 | 
				
			||||||
 | 
					CREATE TABLE
 | 
				
			||||||
 | 
					    runs (
 | 
				
			||||||
 | 
					        id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					        name TEXT NOT NULL,
 | 
				
			||||||
 | 
					        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Create TestRuns table
 | 
				
			||||||
 | 
					CREATE TABLE
 | 
				
			||||||
 | 
					    test_runs (
 | 
				
			||||||
 | 
					        id SERIAL PRIMARY KEY,
 | 
				
			||||||
 | 
					        run_id INTEGER NOT NULL,
 | 
				
			||||||
 | 
					        test_name TEXT NOT NULL,
 | 
				
			||||||
 | 
					        path TEXT NOT NULL,
 | 
				
			||||||
 | 
					        run_type run_type NOT NULL,
 | 
				
			||||||
 | 
					        success BOOLEAN,
 | 
				
			||||||
 | 
					        pending BOOLEAN default true,
 | 
				
			||||||
 | 
					        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
					        FOREIGN KEY (run_id) REFERENCES runs (id)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
							
								
								
									
										104
									
								
								docker/entrypoint/02-seed.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								docker/entrypoint/02-seed.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,104 @@
 | 
				
			||||||
 | 
					-- Insert example data into runs table
 | 
				
			||||||
 | 
					INSERT INTO runs (name,created_at) VALUES
 | 
				
			||||||
 | 
					    ('Test Suite Run 1', (now() - interval '1 min')),
 | 
				
			||||||
 | 
					    ('Test Suite Run 2', (now() - interval '1.5 min')),
 | 
				
			||||||
 | 
					    ('Integration Tests', (now() - interval '1 days 2 min'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Insert example data into test_runs table
 | 
				
			||||||
 | 
					INSERT INTO test_runs (run_id, test_name, path, run_type, created_at, success, pending ) VALUES
 | 
				
			||||||
 | 
					    (1, 'Login Test', '/tests/auth/login.test.ts', 'start', (now()), true, false),
 | 
				
			||||||
 | 
					    (1, 'Login Test', '/tests/auth/login.test.ts', 'end', (now() + interval '8 second'), true, false),
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    (1, 'Registration Test', '/tests/auth/register.test.ts', 'start', (now() + interval '10 second'), true, false),
 | 
				
			||||||
 | 
					    (1, 'Registration Test', '/tests/auth/register.test.ts', 'end', (now() + interval '25 second'), true, false),
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    (1, 'Password Reset Test', '/tests/auth/password-reset.test.ts', 'start', (now() + interval '30 second'), true, false),
 | 
				
			||||||
 | 
					    (1, 'Password Reset Test', '/tests/auth/password-reset.test.ts', 'end', (now() + interval '42 second'), true, false),
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    (2, 'Login Test', '/tests/auth/login.test.ts', 'start', (now() + interval '1 minute'), true, false),
 | 
				
			||||||
 | 
					    (2, 'Login Test', '/tests/auth/login.test.ts', 'end', (now() + interval '1 minute 12 second'), false, false),
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    (2, 'Registration Test', '/tests/auth/register.test.ts', 'start', (now() + interval '2 minute'), true, false),
 | 
				
			||||||
 | 
					    (2, 'Registration Test', '/tests/auth/register.test.ts', 'end', (now() + interval '2 minute 18 second'), true, false),
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    (2, 'Password Reset Test', '/tests/auth/password-reset.test.ts', 'start', (now() + interval '3 minute'), true, false),
 | 
				
			||||||
 | 
					    (2, 'Password Reset Test', '/tests/auth/password-reset.test.ts', 'end', (now() + interval '5 minute 15.5 second'), false, false),
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    (3, 'API Integration', '/tests/api/integration.test.ts', 'start', (now() + interval '1 hours'), true, false),
 | 
				
			||||||
 | 
					    (3, 'API Integration', '/tests/api/integration.test.ts', 'end', (now() + interval '1 hours 8 minutes'), true, false),
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    (3, 'Database Integration', '/tests/api/database.test.ts', 'start', (now() + interval '1 hours 10 minutes' - interval '1 days'), true, false),
 | 
				
			||||||
 | 
					    (3, 'Database Integration', '/tests/api/database.test.ts', 'end', (now() + interval '1 hours 25 minutes' - interval '1 days'), false, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    -- Create a view that shows test durations
 | 
				
			||||||
 | 
					CREATE VIEW test_durations AS
 | 
				
			||||||
 | 
					SELECT 
 | 
				
			||||||
 | 
					        tr_end.run_id,
 | 
				
			||||||
 | 
					        tr_end.test_name,
 | 
				
			||||||
 | 
					        tr_end.path,
 | 
				
			||||||
 | 
					        EXTRACT(EPOCH FROM age(tr_end.created_at, tr_start.created_at)) as totalSeconds,
 | 
				
			||||||
 | 
					        tr_start.created_at as started_at,
 | 
				
			||||||
 | 
					        tr_end.created_at as ended_at
 | 
				
			||||||
 | 
					    FROM test_runs tr_end
 | 
				
			||||||
 | 
					    JOIN test_runs tr_start 
 | 
				
			||||||
 | 
					        ON tr_start.run_id = tr_end.run_id 
 | 
				
			||||||
 | 
					        AND tr_start.test_name = tr_end.test_name
 | 
				
			||||||
 | 
					        AND tr_start.path = tr_end.path
 | 
				
			||||||
 | 
					        AND tr_start.run_type = 'start'
 | 
				
			||||||
 | 
					        AND tr_end.run_type = 'end';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        -- Create a view that shows total run durations
 | 
				
			||||||
 | 
					CREATE VIEW run_durations AS
 | 
				
			||||||
 | 
					            SELECT
 | 
				
			||||||
 | 
					                r.id as run_id,
 | 
				
			||||||
 | 
					                r.name as run_name,
 | 
				
			||||||
 | 
					                r.created_at as run_date,
 | 
				
			||||||
 | 
					                SUM(td.totalSeconds) as totalSeconds
 | 
				
			||||||
 | 
					            FROM runs r
 | 
				
			||||||
 | 
									LEFT JOIN test_durations td ON td.run_id = r.id
 | 
				
			||||||
 | 
					            GROUP BY r.id, r.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- Create a view that returns JSON-structured test results
 | 
				
			||||||
 | 
					CREATE VIEW formatted_test_results AS
 | 
				
			||||||
 | 
					WITH test_results AS (
 | 
				
			||||||
 | 
					    SELECT tr.run_id, tr.test_name, tr.path, tr.success, td.totalSeconds
 | 
				
			||||||
 | 
					    FROM test_runs tr
 | 
				
			||||||
 | 
					    JOIN test_durations td ON td.run_id = tr.run_id 
 | 
				
			||||||
 | 
					        AND td.test_name = tr.test_name
 | 
				
			||||||
 | 
					        AND td.path = tr.path
 | 
				
			||||||
 | 
					    WHERE tr.run_type = 'end'
 | 
				
			||||||
 | 
					),
 | 
				
			||||||
 | 
					test_runs_json AS (
 | 
				
			||||||
 | 
					    SELECT 
 | 
				
			||||||
 | 
					        rd.run_id, rd.run_name, rd.totalSeconds, rd.run_date, SUM(CASE WHEN tr.success THEN 1 ELSE 0 END) as success_count,
 | 
				
			||||||
 | 
					        SUM(CASE WHEN NOT tr.success THEN 1 ELSE 0 END) as failure_count,
 | 
				
			||||||
 | 
					        json_agg(
 | 
				
			||||||
 | 
					            json_build_object(
 | 
				
			||||||
 | 
					                'test_name', tr.test_name,
 | 
				
			||||||
 | 
					                'path', tr.path,
 | 
				
			||||||
 | 
					                'duration', tr.totalSeconds,
 | 
				
			||||||
 | 
					                -- 'duration_seconds', ROUND(EXTRACT(EPOCH FROM tr.duration)::numeric, 2),
 | 
				
			||||||
 | 
					                'status', CASE WHEN tr.success THEN 'success' ELSE 'failure' END
 | 
				
			||||||
 | 
					            ) ORDER BY tr.test_name
 | 
				
			||||||
 | 
					        ) as children
 | 
				
			||||||
 | 
					    FROM run_durations rd
 | 
				
			||||||
 | 
					    JOIN test_results tr ON tr.run_id = rd.run_id
 | 
				
			||||||
 | 
					    GROUP BY rd.run_id, rd.run_name, rd.totalSeconds, rd.run_date
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					SELECT json_build_object(
 | 
				
			||||||
 | 
					    'run_id', trj.run_id,
 | 
				
			||||||
 | 
					    'run_name', trj.run_name,
 | 
				
			||||||
 | 
					    'date', trj.run_date,
 | 
				
			||||||
 | 
					    'total_duration', trj.totalSeconds,
 | 
				
			||||||
 | 
					    'success_count', trj.success_count,
 | 
				
			||||||
 | 
					    'failure_count', trj.failure_count,
 | 
				
			||||||
 | 
					    'children', trj.children
 | 
				
			||||||
 | 
					) as result
 | 
				
			||||||
 | 
					FROM test_runs_json trj
 | 
				
			||||||
 | 
					ORDER BY trj.run_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"devDependencies":{"cypress":"^14.3.2"}}
 | 
				
			||||||
							
								
								
									
										1354
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1354
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										2
									
								
								pnpm-workspace.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								pnpm-workspace.yaml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					ignoredBuiltDependencies:
 | 
				
			||||||
 | 
					  - cypress
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue