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"
 | 
					            "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 pool from "@/utils/db";
 | 
				
			||||||
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function GET() {
 | 
					export async function GET() {
 | 
				
			||||||
  const runResults = await pool.query<{
 | 
					  const runResults = await pool
 | 
				
			||||||
    run_id: number;
 | 
					    .query<{
 | 
				
			||||||
    run_name: string;
 | 
					      id: number;
 | 
				
			||||||
    run_date: string;
 | 
					      name: string;
 | 
				
			||||||
    totalseconds: string;
 | 
					      created_at: Date;
 | 
				
			||||||
  }>(`
 | 
					    }>(`select * from runs;`)
 | 
				
			||||||
        SELECT
 | 
					    .then((r) => r.rows);
 | 
				
			||||||
          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
 | 
					  const testResults = await pool
 | 
				
			||||||
  // Get start times for each test
 | 
					    .query<{
 | 
				
			||||||
  const startTimes = await pool.query<{
 | 
					      id: number;
 | 
				
			||||||
    run_id: number;
 | 
					      run_id: number;
 | 
				
			||||||
    test_name: string;
 | 
					      test_name: string;
 | 
				
			||||||
    path: string;
 | 
					      path: string;
 | 
				
			||||||
    start_time: Date;
 | 
					      duration: number;
 | 
				
			||||||
  }>(`
 | 
					      success?: boolean;
 | 
				
			||||||
    SELECT 
 | 
					      pending: boolean;
 | 
				
			||||||
      run_id,
 | 
					      created_at: Date;
 | 
				
			||||||
      test_name,
 | 
					    }>(`select * from test_runs;`)
 | 
				
			||||||
      path,
 | 
					    .then((r) => r.rows);
 | 
				
			||||||
      created_at as start_time
 | 
					 | 
				
			||||||
    FROM test_runs
 | 
					 | 
				
			||||||
    WHERE run_type = 'start'
 | 
					 | 
				
			||||||
  `);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get end times and results for each test
 | 
					  const formatted = runResults.map((run) => {
 | 
				
			||||||
  const endResults = await pool.query<{
 | 
					    const children = testResults.filter(
 | 
				
			||||||
    run_id: number;
 | 
					      (test) => test !== null && test.run_id === run.id
 | 
				
			||||||
    test_name: string;
 | 
					    );
 | 
				
			||||||
    path: string;
 | 
					    const totalDuration = children.reduce(
 | 
				
			||||||
    end_time: Date;
 | 
					      (n, child) => n + Number(child.duration),
 | 
				
			||||||
    success: boolean;
 | 
					      0
 | 
				
			||||||
    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 {
 | 
					    return {
 | 
				
			||||||
      run_id: run.run_id,
 | 
					      run_id: run.id,
 | 
				
			||||||
      run_name: run.run_name,
 | 
					      run_name: run.name,
 | 
				
			||||||
      date: run.run_date,
 | 
					      date: run.created_at,
 | 
				
			||||||
      totalseconds: run.totalseconds,
 | 
					      totalseconds: totalDuration,
 | 
				
			||||||
      success_count: runTests.filter((test) => test.success).length,
 | 
					      success_count: children.filter((test) => test.success).length,
 | 
				
			||||||
      failure_count: runTests.filter((test) => !test.success).length,
 | 
					      failure_count: children.filter((test) => !test.success).length,
 | 
				
			||||||
      children: runTests.map((test) => ({
 | 
					      children: children.map((test) => ({
 | 
				
			||||||
        test_name: test.test_name,
 | 
					        ...test,
 | 
				
			||||||
        path: test.path,
 | 
					        duration: Number(test.duration),
 | 
				
			||||||
        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);
 | 
					  return NextResponse.json(formatted);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,14 +6,14 @@ export async function POST(request: Request) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    let {
 | 
					    let {
 | 
				
			||||||
      runName,
 | 
					      runName,
 | 
				
			||||||
 | 
					      testId,
 | 
				
			||||||
      testName,
 | 
					      testName,
 | 
				
			||||||
      path,
 | 
					      path,
 | 
				
			||||||
 | 
					      duration,
 | 
				
			||||||
      success,
 | 
					      success,
 | 
				
			||||||
      runId: passedRunId,
 | 
					      runId: passedRunId,
 | 
				
			||||||
      runType = "start"
 | 
					 | 
				
			||||||
    } = await request.json();
 | 
					    } = await request.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let pending;
 | 
					 | 
				
			||||||
    if (!passedRunId) {
 | 
					    if (!passedRunId) {
 | 
				
			||||||
      const res = await pool
 | 
					      const res = await pool
 | 
				
			||||||
        .query(
 | 
					        .query(
 | 
				
			||||||
| 
						 | 
					@ -23,30 +23,34 @@ export async function POST(request: Request) {
 | 
				
			||||||
          RETURNING id
 | 
					          RETURNING id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        SELECT id FROM new_run;`,
 | 
					        SELECT id FROM new_run;`,
 | 
				
			||||||
          [runName]
 | 
					          [runName],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .then((r) => r.rows[0].id);
 | 
					        .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
 | 
					    if (!testId) {
 | 
				
			||||||
    // can likely remove runType as a whole? 
 | 
					      const res = await pool.query(
 | 
				
			||||||
 | 
					        `INSERT INTO test_runs (run_id, test_name, path)
 | 
				
			||||||
 | 
					         VALUES ($1, $2, $3) returning id`,
 | 
				
			||||||
    if (runType === "end") pending = false;
 | 
					        [passedRunId, testName, path],
 | 
				
			||||||
    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({
 | 
				
			||||||
 | 
					        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 });
 | 
					    return NextResponse.json({ runId });
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error creating run:", error);
 | 
					    console.error("Error creating run:", error);
 | 
				
			||||||
| 
						 | 
					@ -58,7 +62,7 @@ export async function POST(request: Request) {
 | 
				
			||||||
        // @ts-ignore
 | 
					        // @ts-ignore
 | 
				
			||||||
        detail: error.detail,
 | 
					        detail: error.detail,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      { status: 500 }
 | 
					      { status: 500 },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
import "@/styles/globals.css";
 | 
					import "@/styles/globals.css";
 | 
				
			||||||
import type { Metadata } from "next";
 | 
					 | 
				
			||||||
import { Geist } from "next/font/google";
 | 
					import { Geist } from "next/font/google";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -13,6 +12,7 @@ import { useEffect } from "react";
 | 
				
			||||||
const queryClient = new QueryClient({
 | 
					const queryClient = new QueryClient({
 | 
				
			||||||
  defaultOptions: {
 | 
					  defaultOptions: {
 | 
				
			||||||
    queries: {
 | 
					    queries: {
 | 
				
			||||||
 | 
					      refetchInterval: 1000 * 1, // Useful in Prod so it updates every second and shows pending states
 | 
				
			||||||
      refetchOnWindowFocus: true,
 | 
					      refetchOnWindowFocus: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,6 @@ export default function RootLayout({
 | 
				
			||||||
    focusManager.setEventListener((handleFocus) => {
 | 
					    focusManager.setEventListener((handleFocus) => {
 | 
				
			||||||
      const focus = () => {
 | 
					      const focus = () => {
 | 
				
			||||||
        if (!document.hidden) {
 | 
					        if (!document.hidden) {
 | 
				
			||||||
          // console.log("Focus from parent");
 | 
					 | 
				
			||||||
          handleFocus(true);
 | 
					          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.
 | 
					          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());
 | 
					  return fetch(url).then((r) => r.json());
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Update the page component to use full width
 | 
					 | 
				
			||||||
export default function Home() {
 | 
					export default function Home() {
 | 
				
			||||||
  const { isPending, error, data, isError } = useQuery({
 | 
					  const { isPending, error, data, isError } = useQuery({
 | 
				
			||||||
    queryKey: ["historicData"],
 | 
					    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">
 | 
					    <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">
 | 
					      <div className="w-full max-w-4xl mx-auto">
 | 
				
			||||||
        <h1 className="text-2xl font-semibold mb-6 text-center">
 | 
					        <h1 className="text-2xl font-semibold mb-6 text-center">
 | 
				
			||||||
          {/* Corporate Planning AG - Historic Test Data */}
 | 
					 | 
				
			||||||
          Historic Test Data
 | 
					          Historic Test Data
 | 
				
			||||||
        </h1>
 | 
					        </h1>
 | 
				
			||||||
        <div className="">
 | 
					        <div className="">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ import {
 | 
				
			||||||
} from "@/components/ui/collapsible";
 | 
					} from "@/components/ui/collapsible";
 | 
				
			||||||
import { Button } from "./ui/button";
 | 
					import { Button } from "./ui/button";
 | 
				
			||||||
import type { Result } from "@/lib/types";
 | 
					import type { Result } from "@/lib/types";
 | 
				
			||||||
import toCSV from "@/utils/toCSV";
 | 
					import { handleExportCSV } from "@/utils/handleCSVExport";
 | 
				
			||||||
import { RunItem } from "./run-item";
 | 
					import { RunItem } from "./run-item";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface DateGroupProps {
 | 
					interface DateGroupProps {
 | 
				
			||||||
| 
						 | 
					@ -45,35 +45,6 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [dateGroup]);
 | 
					  }, [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 (
 | 
					  return (
 | 
				
			||||||
    <div className="mb-4 border border-zinc-600 rounded-lg overflow-hidden">
 | 
					    <div className="mb-4 border border-zinc-600 rounded-lg overflow-hidden">
 | 
				
			||||||
      <Collapsible
 | 
					      <Collapsible
 | 
				
			||||||
| 
						 | 
					@ -89,7 +60,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
				
			||||||
              size="sm"
 | 
					              size="sm"
 | 
				
			||||||
              onClick={(e: React.MouseEvent) => {
 | 
					              onClick={(e: React.MouseEvent) => {
 | 
				
			||||||
                e.stopPropagation();
 | 
					                e.stopPropagation();
 | 
				
			||||||
                handleExportCSV();
 | 
					                handleExportCSV(dateGroup);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
              className="flex items-center gap-2"
 | 
					              className="flex items-center gap-2"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
| 
						 | 
					@ -110,7 +81,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
				
			||||||
            <ChevronDown
 | 
					            <ChevronDown
 | 
				
			||||||
              className={cn(
 | 
					              className={cn(
 | 
				
			||||||
                "h-4 w-4 text-zinc-100 transition-transform duration-200",
 | 
					                "h-4 w-4 text-zinc-100 transition-transform duration-200",
 | 
				
			||||||
                isOpen && "rotate-180"
 | 
					                isOpen && "rotate-180",
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -120,7 +91,7 @@ export function DateGroup({ dateGroup }: DateGroupProps) {
 | 
				
			||||||
            {dateGroup.runs
 | 
					            {dateGroup.runs
 | 
				
			||||||
              .sort(
 | 
					              .sort(
 | 
				
			||||||
                (a, b) =>
 | 
					                (a, b) =>
 | 
				
			||||||
                  new Date(b.date).getTime() - new Date(a.date).getTime()
 | 
					                  new Date(b.date).getTime() - new Date(a.date).getTime(),
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
              .map((run) => (
 | 
					              .map((run) => (
 | 
				
			||||||
                <RunItem key={run.run_id} result={run} />
 | 
					                <RunItem key={run.run_id} result={run} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,13 @@ interface FoldableListProps {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FoldableList({ results, className }: 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)
 | 
					  if (!results[0]?.run_id)
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <h1 className="text-center text-red-400">
 | 
					      <h1 className="text-center text-red-400">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,11 @@ export function RunItem({ result }: RunItemProps) {
 | 
				
			||||||
  const successPercentage =
 | 
					  const successPercentage =
 | 
				
			||||||
    totalTests > 0 ? (result.success_count / totalTests) * 100 : 0;
 | 
					    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 hours: number | string = Math.floor((seconds as number) / 3600);
 | 
				
			||||||
    var minutes: number | string = Math.floor(
 | 
					    var minutes: number | string = Math.floor(
 | 
				
			||||||
      ((seconds as number) - hours * 3600) / 60
 | 
					      ((seconds as number) - hours * 3600) / 60
 | 
				
			||||||
| 
						 | 
					@ -92,7 +96,7 @@ export function RunItem({ result }: RunItemProps) {
 | 
				
			||||||
                      "w-2 h-2 rounded-full",
 | 
					                      "w-2 h-2 rounded-full",
 | 
				
			||||||
                      child.pending
 | 
					                      child.pending
 | 
				
			||||||
                        ? "bg-blue-500"
 | 
					                        ? "bg-blue-500"
 | 
				
			||||||
                        : child.status === "success"
 | 
					                        : child.success
 | 
				
			||||||
                        ? "bg-green-500"
 | 
					                        ? "bg-green-500"
 | 
				
			||||||
                        : "bg-red-500"
 | 
					                        : "bg-red-500"
 | 
				
			||||||
                    )}
 | 
					                    )}
 | 
				
			||||||
| 
						 | 
					@ -102,7 +106,7 @@ export function RunItem({ result }: RunItemProps) {
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="flex items-center gap-4 pr-8">
 | 
					                <div className="flex items-center gap-4 pr-8">
 | 
				
			||||||
                  <span className="text-sm text-zinc-300 w-12 text-right">
 | 
					                  <span className="text-sm text-zinc-300 w-12 text-right">
 | 
				
			||||||
                    {formatSeconds(child.totalseconds)}
 | 
					                    {formatSeconds(child.duration)}
 | 
				
			||||||
                  </span>
 | 
					                  </span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,11 +6,12 @@ export type Result = {
 | 
				
			||||||
  success_count: number;
 | 
					  success_count: number;
 | 
				
			||||||
  failure_count: number;
 | 
					  failure_count: number;
 | 
				
			||||||
  children: Array<{
 | 
					  children: Array<{
 | 
				
			||||||
 | 
					    id: number;
 | 
				
			||||||
 | 
					    run_id: number;
 | 
				
			||||||
    test_name: string;
 | 
					    test_name: string;
 | 
				
			||||||
    path: string;
 | 
					    path: string;
 | 
				
			||||||
    totalseconds: number;
 | 
					    duration?: number;
 | 
				
			||||||
    percentage_of_total: number;
 | 
					    success?: boolean;
 | 
				
			||||||
    status: "success" | "failure";
 | 
					 | 
				
			||||||
    pending: boolean
 | 
					    pending: boolean
 | 
				
			||||||
  }>;
 | 
					  }>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import "../env";
 | 
					import "../env";
 | 
				
			||||||
import { Pool } from "pg";
 | 
					import { Pool } from "pg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// console.log(process.env)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const pool = new Pool({
 | 
					const pool = new Pool({
 | 
				
			||||||
  connectionString: process.env.NEXT_PUBLIC_DATABASE_URL,
 | 
					  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[],
 | 
					  headers: string[],
 | 
				
			||||||
  rows: { date: string; runs: Result[] }
 | 
					  rows: { date: string; runs: Result[] }
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
 | 
					  console.log(rows);
 | 
				
			||||||
  const a = rows.runs.flatMap((run) =>
 | 
					  const a = rows.runs.flatMap((run) =>
 | 
				
			||||||
    run.children.map((child) => [
 | 
					    run.children.map((child) => [
 | 
				
			||||||
      run.run_name,
 | 
					      run.run_name,
 | 
				
			||||||
      child.test_name,
 | 
					      child.test_name,
 | 
				
			||||||
      child.path,
 | 
					      child.path,
 | 
				
			||||||
      child.status,
 | 
					      child.success? "true" : "false",
 | 
				
			||||||
      child.totalseconds,
 | 
					      child.duration!/1000
 | 
				
			||||||
      child.percentage_of_total,
 | 
					 | 
				
			||||||
    ])
 | 
					    ])
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										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,69 +61,79 @@ function setupApiRequestPlugin(on, config) {
 | 
				
			||||||
    console.log("API plugin event handlers already registered, skipping...");
 | 
					    console.log("API plugin event handlers already registered, skipping...");
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Track whether we've already run the before all tests request
 | 
					  // Track whether we've already run the before all tests request
 | 
				
			||||||
  let beforeAllTestsExecuted = false;
 | 
					  let beforeAllTestsExecuted = false;
 | 
				
			||||||
  // Track the current run ID across all tests
 | 
					  // Globals to store the current run ID and test ID
 | 
				
			||||||
  let currentRunId = null;
 | 
					  let currentRunId, currentTestId, currentTestName = null;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Before all tests run (only once)
 | 
					  // Before all tests run (only once)
 | 
				
			||||||
  on("before:run", async () => {
 | 
					  on("before:run", async () => {
 | 
				
			||||||
    if (!beforeAllTestsExecuted) {
 | 
					    if (!beforeAllTestsExecuted) {
 | 
				
			||||||
      apiRequests.beforeAllTests = {
 | 
					      apiRequests.beforeAllTests = {
 | 
				
			||||||
        url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
					        url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        body: {
 | 
					        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()}`,
 | 
					          runName: `Cypress Run @ ${new Date().toLocaleTimeString()}`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					      const res = await makeApiRequest(apiRequests.beforeAllTests,"beforeAllTests",);
 | 
				
			||||||
      const res = await makeApiRequest(
 | 
					      // Store the run ID for later use
 | 
				
			||||||
        apiRequests.beforeAllTests,
 | 
					      // This ID is used to associate all test files with this run
 | 
				
			||||||
        "beforeAllTests",
 | 
					      // and is passed to the beforeTestFile and afterTestFile requests
 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      currentRunId = res.runId;
 | 
					      currentRunId = res.runId;
 | 
				
			||||||
 | 
					 | 
				
			||||||
      beforeAllTestsExecuted = true;
 | 
					      beforeAllTestsExecuted = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO: FIGURE OUT WHY THIS RUNS TWICE
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Process individual test events
 | 
					  // Process individual test events
 | 
				
			||||||
  on("before:spec", async (test) => {
 | 
					  on("before:spec", async (test) => {
 | 
				
			||||||
 | 
					    // If the run ID is not set, skip this request
 | 
				
			||||||
    if (!currentRunId) return;
 | 
					    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 = {
 | 
					    const testRequest = {
 | 
				
			||||||
      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
					      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      body: {
 | 
					      body: {
 | 
				
			||||||
 | 
					        // This Request creates a new Test, it will be marked as pending
 | 
				
			||||||
 | 
					        // in the Database and on the Frontend
 | 
				
			||||||
        runId: currentRunId,
 | 
					        runId: currentRunId,
 | 
				
			||||||
        testName: test.name,
 | 
					        testName: test.name,
 | 
				
			||||||
        path: test.relative,
 | 
					        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) => {
 | 
					  on("after:spec", async (test, results) => {
 | 
				
			||||||
    if (!currentRunId) return;
 | 
					    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 = {
 | 
					    const testRequest = {
 | 
				
			||||||
      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
					      url: `${config.historyTrackerURL || ""}/api/run/create`,
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      body: {
 | 
					      body: {
 | 
				
			||||||
        runId: currentRunId,
 | 
					        runId: currentRunId,
 | 
				
			||||||
        testName: test.name,
 | 
					        testId: currentTestId, // This request updates the Test created in beforeTestFile
 | 
				
			||||||
        path: test.relative,
 | 
					        testName: test.name, 
 | 
				
			||||||
        runType: "end",
 | 
					        path: test.relative, 
 | 
				
			||||||
 | 
					        // 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,
 | 
					        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");
 | 
					    await makeApiRequest(testRequest, "afterTest");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,11 +3,11 @@ DROP TABLE IF EXISTS test_runs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DROP TABLE IF EXISTS runs;
 | 
					DROP TABLE IF EXISTS runs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Drop existing enum if it exists
 | 
					-- -- Drop existing enum if it exists
 | 
				
			||||||
DROP TYPE IF EXISTS run_type;
 | 
					-- DROP TYPE IF EXISTS run_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Create an ENUM type for RunType
 | 
					-- -- Create an ENUM type for RunType
 | 
				
			||||||
CREATE TYPE run_type AS ENUM ('start', 'end');
 | 
					-- CREATE TYPE run_type AS ENUM ('start', 'end');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Create Runs table
 | 
					-- Create Runs table
 | 
				
			||||||
CREATE TABLE
 | 
					CREATE TABLE
 | 
				
			||||||
| 
						 | 
					@ -24,9 +24,13 @@ CREATE TABLE
 | 
				
			||||||
        run_id INTEGER NOT NULL,
 | 
					        run_id INTEGER NOT NULL,
 | 
				
			||||||
        test_name TEXT NOT NULL,
 | 
					        test_name TEXT NOT NULL,
 | 
				
			||||||
        path TEXT NOT NULL,
 | 
					        path TEXT NOT NULL,
 | 
				
			||||||
        run_type run_type NOT NULL,
 | 
					        duration bigint,
 | 
				
			||||||
        success BOOLEAN,
 | 
					        success BOOLEAN,
 | 
				
			||||||
        pending BOOLEAN default true,
 | 
					        pending BOOLEAN default true,
 | 
				
			||||||
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
					        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
        FOREIGN KEY (run_id) REFERENCES runs (id)
 | 
					        FOREIGN KEY (run_id) REFERENCES runs (id)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
| 
						 | 
					@ -1,104 +1,104 @@
 | 
				
			||||||
-- Insert example data into runs table
 | 
					-- -- Insert example data into runs table
 | 
				
			||||||
INSERT INTO runs (name,created_at) VALUES
 | 
					-- INSERT INTO runs (name,created_at) VALUES
 | 
				
			||||||
    ('Test Suite Run 1', (now() - interval '1 min')),
 | 
					--     ('Test Suite Run 1', (now() - interval '1 min')),
 | 
				
			||||||
    ('Test Suite Run 2', (now() - interval '1.5 min')),
 | 
					--     ('Test Suite Run 2', (now() - interval '1.5 min')),
 | 
				
			||||||
    ('Integration Tests', (now() - interval '1 days 2 min'));
 | 
					--     ('Integration Tests', (now() - interval '1 days 2 min'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Insert example data into test_runs table
 | 
					-- -- Insert example data into test_runs table
 | 
				
			||||||
INSERT INTO test_runs (run_id, test_name, path, run_type, created_at, success, pending ) VALUES
 | 
					-- 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', 'start', (now()), true, false),
 | 
				
			||||||
    (1, 'Login Test', '/tests/auth/login.test.ts', 'end', (now() + interval '8 second'), 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', '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', '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', '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', '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', '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', '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', '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', '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', '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', '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', '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', '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', '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', 'end', (now() + interval '1 hours 25 minutes' - interval '1 days'), false, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    -- Create a view that shows test durations
 | 
					--     -- Create a view that shows test durations
 | 
				
			||||||
CREATE VIEW test_durations AS
 | 
					-- CREATE VIEW test_durations AS
 | 
				
			||||||
SELECT 
 | 
					-- SELECT 
 | 
				
			||||||
        tr_end.run_id,
 | 
					--         tr_end.run_id,
 | 
				
			||||||
        tr_end.test_name,
 | 
					--         tr_end.test_name,
 | 
				
			||||||
        tr_end.path,
 | 
					--         tr_end.path,
 | 
				
			||||||
        EXTRACT(EPOCH FROM age(tr_end.created_at, tr_start.created_at)) as totalSeconds,
 | 
					--         EXTRACT(EPOCH FROM age(tr_end.created_at, tr_start.created_at)) as totalSeconds,
 | 
				
			||||||
        tr_start.created_at as started_at,
 | 
					--         tr_start.created_at as started_at,
 | 
				
			||||||
        tr_end.created_at as ended_at
 | 
					--         tr_end.created_at as ended_at
 | 
				
			||||||
    FROM test_runs tr_end
 | 
					--     FROM test_runs tr_end
 | 
				
			||||||
    JOIN test_runs tr_start 
 | 
					--     JOIN test_runs tr_start 
 | 
				
			||||||
        ON tr_start.run_id = tr_end.run_id 
 | 
					--         ON tr_start.run_id = tr_end.run_id 
 | 
				
			||||||
        AND tr_start.test_name = tr_end.test_name
 | 
					--         AND tr_start.test_name = tr_end.test_name
 | 
				
			||||||
        AND tr_start.path = tr_end.path
 | 
					--         AND tr_start.path = tr_end.path
 | 
				
			||||||
        AND tr_start.run_type = 'start'
 | 
					--         AND tr_start.run_type = 'start'
 | 
				
			||||||
        AND tr_end.run_type = 'end';
 | 
					--         AND tr_end.run_type = 'end';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        -- Create a view that shows total run durations
 | 
					--         -- Create a view that shows total run durations
 | 
				
			||||||
CREATE VIEW run_durations AS
 | 
					-- CREATE VIEW run_durations AS
 | 
				
			||||||
            SELECT
 | 
					--             SELECT
 | 
				
			||||||
                r.id as run_id,
 | 
					--                 r.id as run_id,
 | 
				
			||||||
                r.name as run_name,
 | 
					--                 r.name as run_name,
 | 
				
			||||||
                r.created_at as run_date,
 | 
					--                 r.created_at as run_date,
 | 
				
			||||||
                SUM(td.totalSeconds) as totalSeconds
 | 
					--                 SUM(td.totalSeconds) as totalSeconds
 | 
				
			||||||
            FROM runs r
 | 
					--             FROM runs r
 | 
				
			||||||
				LEFT JOIN test_durations td ON td.run_id = r.id
 | 
					-- 				LEFT JOIN test_durations td ON td.run_id = r.id
 | 
				
			||||||
            GROUP BY r.id, r.name;
 | 
					--             GROUP BY r.id, r.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Create a view that returns JSON-structured test results
 | 
					-- -- Create a view that returns JSON-structured test results
 | 
				
			||||||
CREATE VIEW formatted_test_results AS
 | 
					-- CREATE VIEW formatted_test_results AS
 | 
				
			||||||
WITH test_results AS (
 | 
					-- WITH test_results AS (
 | 
				
			||||||
    SELECT tr.run_id, tr.test_name, tr.path, tr.success, td.totalSeconds
 | 
					--     SELECT tr.run_id, tr.test_name, tr.path, tr.success, td.totalSeconds
 | 
				
			||||||
    FROM test_runs tr
 | 
					--     FROM test_runs tr
 | 
				
			||||||
    JOIN test_durations td ON td.run_id = tr.run_id 
 | 
					--     JOIN test_durations td ON td.run_id = tr.run_id 
 | 
				
			||||||
        AND td.test_name = tr.test_name
 | 
					--         AND td.test_name = tr.test_name
 | 
				
			||||||
        AND td.path = tr.path
 | 
					--         AND td.path = tr.path
 | 
				
			||||||
    WHERE tr.run_type = 'end'
 | 
					--     WHERE tr.run_type = 'end'
 | 
				
			||||||
),
 | 
					-- ),
 | 
				
			||||||
test_runs_json AS (
 | 
					-- test_runs_json AS (
 | 
				
			||||||
    SELECT 
 | 
					--     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,
 | 
					--         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,
 | 
					--         SUM(CASE WHEN NOT tr.success THEN 1 ELSE 0 END) as failure_count,
 | 
				
			||||||
        json_agg(
 | 
					--         json_agg(
 | 
				
			||||||
            json_build_object(
 | 
					--             json_build_object(
 | 
				
			||||||
                'test_name', tr.test_name,
 | 
					--                 'test_name', tr.test_name,
 | 
				
			||||||
                'path', tr.path,
 | 
					--                 'path', tr.path,
 | 
				
			||||||
                'duration', tr.totalSeconds,
 | 
					--                 'duration', tr.totalSeconds,
 | 
				
			||||||
                -- 'duration_seconds', ROUND(EXTRACT(EPOCH FROM tr.duration)::numeric, 2),
 | 
					--                 -- 'duration_seconds', ROUND(EXTRACT(EPOCH FROM tr.duration)::numeric, 2),
 | 
				
			||||||
                'status', CASE WHEN tr.success THEN 'success' ELSE 'failure' END
 | 
					--                 'status', CASE WHEN tr.success THEN 'success' ELSE 'failure' END
 | 
				
			||||||
            ) ORDER BY tr.test_name
 | 
					--             ) ORDER BY tr.test_name
 | 
				
			||||||
        ) as children
 | 
					--         ) as children
 | 
				
			||||||
    FROM run_durations rd
 | 
					--     FROM run_durations rd
 | 
				
			||||||
    JOIN test_results tr ON tr.run_id = rd.run_id
 | 
					--     JOIN test_results tr ON tr.run_id = rd.run_id
 | 
				
			||||||
    GROUP BY rd.run_id, rd.run_name, rd.totalSeconds, rd.run_date
 | 
					--     GROUP BY rd.run_id, rd.run_name, rd.totalSeconds, rd.run_date
 | 
				
			||||||
)
 | 
					-- )
 | 
				
			||||||
SELECT json_build_object(
 | 
					-- SELECT json_build_object(
 | 
				
			||||||
    'run_id', trj.run_id,
 | 
					--     'run_id', trj.run_id,
 | 
				
			||||||
    'run_name', trj.run_name,
 | 
					--     'run_name', trj.run_name,
 | 
				
			||||||
    'date', trj.run_date,
 | 
					--     'date', trj.run_date,
 | 
				
			||||||
    'total_duration', trj.totalSeconds,
 | 
					--     'total_duration', trj.totalSeconds,
 | 
				
			||||||
    'success_count', trj.success_count,
 | 
					--     'success_count', trj.success_count,
 | 
				
			||||||
    'failure_count', trj.failure_count,
 | 
					--     'failure_count', trj.failure_count,
 | 
				
			||||||
    'children', trj.children
 | 
					--     'children', trj.children
 | 
				
			||||||
) as result
 | 
					-- ) as result
 | 
				
			||||||
FROM test_runs_json trj
 | 
					-- FROM test_runs_json trj
 | 
				
			||||||
ORDER BY trj.run_id;
 | 
					-- ORDER BY trj.run_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue