mass
This commit is contained in:
		
							parent
							
								
									8bb5fee1c0
								
							
						
					
					
						commit
						26375a13b1
					
				
					 19 changed files with 499 additions and 296 deletions
				
			
		
							
								
								
									
										21
									
								
								.github/workflows/frontend-build.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/frontend-build.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
name: Build Next.js App
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [master]
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node.js environment
 | 
			
		||||
        uses: actions/setup-node@v1
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 18
 | 
			
		||||
 | 
			
		||||
      - name: Install Dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
 | 
			
		||||
      - name: Build Next App
 | 
			
		||||
        run: npm run build
 | 
			
		||||
							
								
								
									
										71
									
								
								.github/workflows/test-environment.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								.github/workflows/test-environment.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
name: Test Environment Setup
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ main ]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [ main ]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  setup-test-environment:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    services:
 | 
			
		||||
      postgres:
 | 
			
		||||
        image: postgres:latest
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_DB: schoolproject
 | 
			
		||||
          POSTGRES_USER: postgres
 | 
			
		||||
          POSTGRES_PASSWORD: postgres
 | 
			
		||||
        ports:
 | 
			
		||||
          - 5432:5432
 | 
			
		||||
        options: >-
 | 
			
		||||
          --health-cmd pg_isready
 | 
			
		||||
          --health-interval 10s
 | 
			
		||||
          --health-timeout 5s
 | 
			
		||||
          --health-retries 5
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
    - name: Setup Node.js
 | 
			
		||||
      uses: actions/setup-node@v4
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: '20'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
    - name: Setup PNPM
 | 
			
		||||
      uses: pnpm/action-setup@v3
 | 
			
		||||
      with:
 | 
			
		||||
        version: 10.8.0
 | 
			
		||||
        run_install: false
 | 
			
		||||
 | 
			
		||||
    - name: Install dependencies
 | 
			
		||||
      run: pnpm install
 | 
			
		||||
 | 
			
		||||
    - name: Setup Backend
 | 
			
		||||
      run: |
 | 
			
		||||
        cd Backend
 | 
			
		||||
        cp .env.example .env
 | 
			
		||||
        pnpm build
 | 
			
		||||
 | 
			
		||||
    - name: Run Tests
 | 
			
		||||
      run: |
 | 
			
		||||
        # Backend tests
 | 
			
		||||
        cd Backend
 | 
			
		||||
        pnpm test || echo "No backend tests found or tests failed"
 | 
			
		||||
        
 | 
			
		||||
        # Frontend tests with Cypress
 | 
			
		||||
        cd ../Client
 | 
			
		||||
        pnpm cypress run || echo "No frontend tests found or tests failed"
 | 
			
		||||
 | 
			
		||||
    - name: Upload test results
 | 
			
		||||
      if: always()
 | 
			
		||||
      uses: actions/upload-artifact@v4
 | 
			
		||||
      with:
 | 
			
		||||
        name: test-results
 | 
			
		||||
        path: |
 | 
			
		||||
          Backend/test-results
 | 
			
		||||
          Client/cypress/screenshots
 | 
			
		||||
          Client/cypress/videos
 | 
			
		||||
							
								
								
									
										17
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -11,5 +11,20 @@
 | 
			
		|||
            "password": "postgres"
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "sqltools.results.reuseTabs": "connection"
 | 
			
		||||
    "sqltools.results.reuseTabs": "connection",
 | 
			
		||||
    "files.exclude": {
 | 
			
		||||
        "**/.env*": true,
 | 
			
		||||
        "**/.next": true,
 | 
			
		||||
        "**/.pnpm-store": true,
 | 
			
		||||
        "**/.vscode": true,
 | 
			
		||||
        "**/*.config*": true,
 | 
			
		||||
        "**/biome.jsonc": true,
 | 
			
		||||
        "**/next-env.d.ts": true,
 | 
			
		||||
        "**/node_modules": true,
 | 
			
		||||
        "**/pnpm-*": true,
 | 
			
		||||
        "**/README.md": true,
 | 
			
		||||
        "**/tsconfig.json": true,
 | 
			
		||||
        "docker/ConnectionTests": true,
 | 
			
		||||
        "docker/db-database": true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,127 +1,50 @@
 | 
			
		|||
 | 
			
		||||
import pool from "@/utils/db";
 | 
			
		||||
import { NextResponse } from "next/server";
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
      `);
 | 
			
		||||
  const runResults = await pool
 | 
			
		||||
    .query<{
 | 
			
		||||
      id: number;
 | 
			
		||||
      name: string;
 | 
			
		||||
      created_at: Date;
 | 
			
		||||
    }>(`select * from runs;`)
 | 
			
		||||
    .then((r) => r.rows);
 | 
			
		||||
 | 
			
		||||
  // 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'
 | 
			
		||||
  `);
 | 
			
		||||
  const testResults = await pool
 | 
			
		||||
    .query<{
 | 
			
		||||
      id: number;
 | 
			
		||||
      run_id: number;
 | 
			
		||||
      test_name: string;
 | 
			
		||||
      path: string;
 | 
			
		||||
      duration: number;
 | 
			
		||||
      success?: boolean;
 | 
			
		||||
      pending: boolean;
 | 
			
		||||
      created_at: Date;
 | 
			
		||||
    }>(`select * from test_runs;`)
 | 
			
		||||
    .then((r) => r.rows);
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
  const formatted = runResults.map((run) => {
 | 
			
		||||
    const children = testResults.filter(
 | 
			
		||||
      (test) => test !== null && test.run_id === run.id
 | 
			
		||||
    );
 | 
			
		||||
    const totalDuration = children.reduce(
 | 
			
		||||
      (n, child) => n + Number(child.duration),
 | 
			
		||||
      0
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
      run_id: run.id,
 | 
			
		||||
      run_name: run.name,
 | 
			
		||||
      date: run.created_at,
 | 
			
		||||
      totalseconds: totalDuration,
 | 
			
		||||
      success_count: children.filter((test) => test.success).length,
 | 
			
		||||
      failure_count: children.filter((test) => !test.success).length,
 | 
			
		||||
      children: children.map((test) => ({
 | 
			
		||||
        ...test,
 | 
			
		||||
        duration: Number(test.duration),
 | 
			
		||||
      })),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return Response.json(formatted);
 | 
			
		||||
  return NextResponse.json(formatted);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,14 @@ export async function POST(request: Request) {
 | 
			
		|||
  try {
 | 
			
		||||
    let {
 | 
			
		||||
      runName,
 | 
			
		||||
      testId,
 | 
			
		||||
      testName,
 | 
			
		||||
      path,
 | 
			
		||||
      duration,
 | 
			
		||||
      success,
 | 
			
		||||
      runId: passedRunId,
 | 
			
		||||
      runType = "start"
 | 
			
		||||
    } = await request.json();
 | 
			
		||||
 | 
			
		||||
    let pending;
 | 
			
		||||
    if (!passedRunId) {
 | 
			
		||||
      const res = await pool
 | 
			
		||||
        .query(
 | 
			
		||||
| 
						 | 
				
			
			@ -23,30 +23,34 @@ export async function POST(request: Request) {
 | 
			
		|||
          RETURNING id
 | 
			
		||||
        )
 | 
			
		||||
        SELECT id FROM new_run;`,
 | 
			
		||||
          [runName]
 | 
			
		||||
          [runName],
 | 
			
		||||
        )
 | 
			
		||||
        .then((r) => r.rows[0].id);
 | 
			
		||||
 | 
			
		||||
        return NextResponse.json({runId: res})
 | 
			
		||||
      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]
 | 
			
		||||
    if (!testId) {
 | 
			
		||||
      const res = await pool.query(
 | 
			
		||||
        `INSERT INTO test_runs (run_id, test_name, path)
 | 
			
		||||
         VALUES ($1, $2, $3) returning id`,
 | 
			
		||||
        [passedRunId, testName, path],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return NextResponse.json({
 | 
			
		||||
        testId: res.rows[0].id,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (testId) {
 | 
			
		||||
      if (duration) {
 | 
			
		||||
        await pool.query(
 | 
			
		||||
          `UPDATE test_runs SET pending = false, success = $3, duration = $2 WHERE id = $1;`,
 | 
			
		||||
          [testId, duration, success],
 | 
			
		||||
        );
 | 
			
		||||
        return NextResponse.json({ testId });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return NextResponse.json({ runId });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error creating run:", error);
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +62,7 @@ export async function POST(request: Request) {
 | 
			
		|||
        // @ts-ignore
 | 
			
		||||
        detail: error.detail,
 | 
			
		||||
      },
 | 
			
		||||
      { status: 500 }
 | 
			
		||||
      { status: 500 },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
"use client";
 | 
			
		||||
import "@/styles/globals.css";
 | 
			
		||||
import type { Metadata } from "next";
 | 
			
		||||
import { Geist } from "next/font/google";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +12,7 @@ import { useEffect } from "react";
 | 
			
		|||
const queryClient = new QueryClient({
 | 
			
		||||
  defaultOptions: {
 | 
			
		||||
    queries: {
 | 
			
		||||
      refetchInterval: 1000 * 1, // Useful in Prod so it updates every second and shows pending states
 | 
			
		||||
      refetchOnWindowFocus: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,6 @@ export default function RootLayout({
 | 
			
		|||
    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.
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,6 @@ 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"],
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +18,6 @@ export default function Home() {
 | 
			
		|||
    <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="">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import {
 | 
			
		|||
} from "@/components/ui/collapsible";
 | 
			
		||||
import { Button } from "./ui/button";
 | 
			
		||||
import type { Result } from "@/lib/types";
 | 
			
		||||
import toCSV from "@/utils/toCSV";
 | 
			
		||||
import { handleExportCSV } from "@/utils/handleCSVExport";
 | 
			
		||||
import { RunItem } from "./run-item";
 | 
			
		||||
 | 
			
		||||
interface DateGroupProps {
 | 
			
		||||
| 
						 | 
				
			
			@ -45,35 +45,6 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
			
		|||
    };
 | 
			
		||||
  }, [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
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +60,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
			
		|||
              size="sm"
 | 
			
		||||
              onClick={(e: React.MouseEvent) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
                handleExportCSV();
 | 
			
		||||
                handleExportCSV(dateGroup);
 | 
			
		||||
              }}
 | 
			
		||||
              className="flex items-center gap-2"
 | 
			
		||||
            >
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +81,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
			
		|||
            <ChevronDown
 | 
			
		||||
              className={cn(
 | 
			
		||||
                "h-4 w-4 text-zinc-100 transition-transform duration-200",
 | 
			
		||||
                isOpen && "rotate-180"
 | 
			
		||||
                isOpen && "rotate-180",
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -120,7 +91,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
			
		|||
            {dateGroup.runs
 | 
			
		||||
              .sort(
 | 
			
		||||
                (a, b) =>
 | 
			
		||||
                  new Date(b.date).getTime() - new Date(a.date).getTime()
 | 
			
		||||
                  new Date(b.date).getTime() - new Date(a.date).getTime(),
 | 
			
		||||
              )
 | 
			
		||||
              .map((run) => (
 | 
			
		||||
                <RunItem key={run.run_id} result={run} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,13 @@ interface FoldableListProps {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function FoldableList({ results, className }: FoldableListProps) {
 | 
			
		||||
  if(!results || results.length === 0)
 | 
			
		||||
    return (
 | 
			
		||||
      <h1 className="text-center text-red-400">
 | 
			
		||||
        No test results found.
 | 
			
		||||
      </h1>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  if (!results[0]?.run_id)
 | 
			
		||||
    return (
 | 
			
		||||
      <h1 className="text-center text-red-400">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,11 @@ export function RunItem({ result }: RunItemProps) {
 | 
			
		|||
  const successPercentage =
 | 
			
		||||
    totalTests > 0 ? (result.success_count / totalTests) * 100 : 0;
 | 
			
		||||
 | 
			
		||||
  const formatSeconds = (seconds: number | string): string => {
 | 
			
		||||
  const formatSeconds = (milliseconds?: number): string => {
 | 
			
		||||
    if(!milliseconds) {
 | 
			
		||||
      return "--:--:--";
 | 
			
		||||
    }
 | 
			
		||||
    var seconds:number|string = milliseconds / 1000;
 | 
			
		||||
    var hours: number | string = Math.floor((seconds as number) / 3600);
 | 
			
		||||
    var minutes: number | string = Math.floor(
 | 
			
		||||
      ((seconds as number) - hours * 3600) / 60
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +96,7 @@ export function RunItem({ result }: RunItemProps) {
 | 
			
		|||
                      "w-2 h-2 rounded-full",
 | 
			
		||||
                      child.pending
 | 
			
		||||
                        ? "bg-blue-500"
 | 
			
		||||
                        : child.status === "success"
 | 
			
		||||
                        : child.success
 | 
			
		||||
                        ? "bg-green-500"
 | 
			
		||||
                        : "bg-red-500"
 | 
			
		||||
                    )}
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +106,7 @@ export function RunItem({ result }: RunItemProps) {
 | 
			
		|||
                </div>
 | 
			
		||||
                <div className="flex items-center gap-4 pr-8">
 | 
			
		||||
                  <span className="text-sm text-zinc-300 w-12 text-right">
 | 
			
		||||
                    {formatSeconds(child.totalseconds)}
 | 
			
		||||
                    {formatSeconds(child.duration)}
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,12 @@ export type Result = {
 | 
			
		|||
  success_count: number;
 | 
			
		||||
  failure_count: number;
 | 
			
		||||
  children: Array<{
 | 
			
		||||
    id: number;
 | 
			
		||||
    run_id: number;
 | 
			
		||||
    test_name: string;
 | 
			
		||||
    path: string;
 | 
			
		||||
    totalseconds: number;
 | 
			
		||||
    percentage_of_total: number;
 | 
			
		||||
    status: "success" | "failure";
 | 
			
		||||
    duration?: number;
 | 
			
		||||
    success?: boolean;
 | 
			
		||||
    pending: boolean
 | 
			
		||||
  }>;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
import "../env";
 | 
			
		||||
import { Pool } from "pg";
 | 
			
		||||
 | 
			
		||||
// console.log(process.env)
 | 
			
		||||
 | 
			
		||||
const pool = new Pool({
 | 
			
		||||
  connectionString: process.env.NEXT_PUBLIC_DATABASE_URL,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										34
									
								
								Backend/src/utils/handleCSVExport.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Backend/src/utils/handleCSVExport.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
"use client";
 | 
			
		||||
 | 
			
		||||
import toCSV from "./toCSV";
 | 
			
		||||
 | 
			
		||||
export const handleExportCSV = (dateGroup: any) => {
 | 
			
		||||
  // Create CSV header
 | 
			
		||||
  const headers = [
 | 
			
		||||
    "Run Name",
 | 
			
		||||
    "Test Name",
 | 
			
		||||
    "Path",
 | 
			
		||||
    "Success",
 | 
			
		||||
    "Duration (s)"
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  console.log(dateGroup);
 | 
			
		||||
  console.log(toCSV(headers, dateGroup));
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -4,14 +4,14 @@ export default function toCSV(
 | 
			
		|||
  headers: string[],
 | 
			
		||||
  rows: { date: string; runs: Result[] }
 | 
			
		||||
) {
 | 
			
		||||
  console.log(rows);
 | 
			
		||||
  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,
 | 
			
		||||
      child.success? "true" : "false",
 | 
			
		||||
      child.duration!/1000
 | 
			
		||||
    ])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										143
									
								
								Client/cypress/e2e/1-getting-started/todo2.cy.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								Client/cypress/e2e/1-getting-started/todo2.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", 3);
 | 
			
		||||
 | 
			
		||||
    // 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");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 82 KiB  | 
| 
						 | 
				
			
			@ -61,12 +61,10 @@ function setupApiRequestPlugin(on, config) {
 | 
			
		|||
    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;
 | 
			
		||||
 | 
			
		||||
  // Globals to store the current run ID and test ID
 | 
			
		||||
  let currentRunId, currentTestId, currentTestName = null;
 | 
			
		||||
  // Before all tests run (only once)
 | 
			
		||||
  on("before:run", async () => {
 | 
			
		||||
    if (!beforeAllTestsExecuted) {
 | 
			
		||||
| 
						 | 
				
			
			@ -74,56 +72,68 @@ function setupApiRequestPlugin(on, config) {
 | 
			
		|||
        url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        body: { 
 | 
			
		||||
          // This Request creates a new Run, which can then be added
 | 
			
		||||
          // to with the beforeTestFile and afterTestFile requests
 | 
			
		||||
          runName: `Cypress Run @ ${new Date().toLocaleTimeString()}`,
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const res = await makeApiRequest(
 | 
			
		||||
        apiRequests.beforeAllTests,
 | 
			
		||||
        "beforeAllTests",
 | 
			
		||||
      );
 | 
			
		||||
      const res = await makeApiRequest(apiRequests.beforeAllTests,"beforeAllTests",);
 | 
			
		||||
      // Store the run ID for later use
 | 
			
		||||
      // This ID is used to associate all test files with this run
 | 
			
		||||
      // and is passed to the beforeTestFile and afterTestFile requests
 | 
			
		||||
      currentRunId = res.runId;
 | 
			
		||||
 | 
			
		||||
      beforeAllTestsExecuted = true;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // TODO: FIGURE OUT WHY THIS RUNS TWICE
 | 
			
		||||
 | 
			
		||||
  // Process individual test events
 | 
			
		||||
  on("before:spec", async (test) => {
 | 
			
		||||
    // If the run ID is not set, skip this request
 | 
			
		||||
    if (!currentRunId) return;
 | 
			
		||||
    // If the test name is the same as the last one, skip this request
 | 
			
		||||
    // This prevents duplicate requests for the same test this is 
 | 
			
		||||
    // important because the beforeTestFile request is made
 | 
			
		||||
    // twice when using cy.visit() (for some reason) (this is a bug in Cypress) 
 | 
			
		||||
    if (currentTestName === test.name) return;
 | 
			
		||||
 | 
			
		||||
    const testRequest = {
 | 
			
		||||
      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      body: {
 | 
			
		||||
        // This Request creates a new Test, it will be marked as pending
 | 
			
		||||
        // in the Database and on the Frontend
 | 
			
		||||
        runId: currentRunId,
 | 
			
		||||
        testName: test.name,
 | 
			
		||||
        path: test.relative,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await makeApiRequest(testRequest, "beforeTest");
 | 
			
		||||
    const res = await makeApiRequest(testRequest, "beforeTest");
 | 
			
		||||
    currentTestId = res.testId;
 | 
			
		||||
    currentTestName = test.name;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  on("after:spec", async (test, results) => {
 | 
			
		||||
    if (!currentRunId) return;
 | 
			
		||||
 | 
			
		||||
    //  after:spec does not have the same bug as before:spec
 | 
			
		||||
    // so we don't need to check for duplicates
 | 
			
		||||
    const testRequest = {
 | 
			
		||||
      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      body: {
 | 
			
		||||
        runId: currentRunId,
 | 
			
		||||
        testId: currentTestId, // This request updates the Test created in beforeTestFile
 | 
			
		||||
        testName: test.name, 
 | 
			
		||||
        path: test.relative, 
 | 
			
		||||
        runType: "end",
 | 
			
		||||
        // This checks if there were any failures,
 | 
			
		||||
        // if there were, it marks the test as failed
 | 
			
		||||
        // otherwise it marks it as passed
 | 
			
		||||
        success: results.stats.failures === 0,
 | 
			
		||||
        // This is the duration of the test in milliseconds 
 | 
			
		||||
        duration: results.stats.duration,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    console.log("specs", results);
 | 
			
		||||
 | 
			
		||||
    await makeApiRequest(testRequest, "afterTest");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,11 +3,11 @@ DROP TABLE IF EXISTS test_runs;
 | 
			
		|||
 | 
			
		||||
DROP TABLE IF EXISTS runs;
 | 
			
		||||
 | 
			
		||||
-- Drop existing enum if it exists
 | 
			
		||||
DROP TYPE IF EXISTS run_type;
 | 
			
		||||
-- -- 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 an ENUM type for RunType
 | 
			
		||||
-- CREATE TYPE run_type AS ENUM ('start', 'end');
 | 
			
		||||
 | 
			
		||||
-- Create Runs table
 | 
			
		||||
CREATE TABLE
 | 
			
		||||
| 
						 | 
				
			
			@ -24,9 +24,13 @@ CREATE TABLE
 | 
			
		|||
        run_id INTEGER NOT NULL,
 | 
			
		||||
        test_name TEXT NOT NULL,
 | 
			
		||||
        path TEXT NOT NULL,
 | 
			
		||||
        run_type run_type NOT NULL,
 | 
			
		||||
        duration bigint,
 | 
			
		||||
        success BOOLEAN,
 | 
			
		||||
        pending BOOLEAN default true,
 | 
			
		||||
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
        FOREIGN KEY (run_id) REFERENCES runs (id)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -1,104 +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 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),
 | 
			
		||||
-- -- 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, '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),
 | 
			
		||||
--     (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, '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, '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),
 | 
			
		||||
--     (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, '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);
 | 
			
		||||
--     (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 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 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;
 | 
			
		||||
-- -- 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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue