Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/build_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
service: [api-gateway, user-course-microservice, genai-backend-microservice, discovery-service]
service: [api-gateway, user-course-microservice, genai-backend-microservice, document-microservice, discovery-service]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -21,6 +21,10 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make gradlew executable
run: chmod +x ./gradlew
working-directory: services/backend/${{ matrix.service }}

- name: Build with Gradle in ${{ matrix.service }}
working-directory: services/backend/${{ matrix.service }}
run: ./gradlew build
Expand All @@ -31,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
service: [client, api-gateway, user-course-microservice, genai-backend-microservice, discovery-service, genai]
service: [client, api-gateway, user-course-microservice, genai-backend-microservice, document-microservice, discovery-service, genai]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
91 changes: 90 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,62 @@ jobs:
./api/scripts/gen-all-v2.sh
working-directory: ${{ github.workspace }} # Ensure command runs from root

# Backend Java unit tests (for each service)
test-backend:
runs-on: ubuntu-latest
strategy:
matrix:
service: [api-gateway, user-course-microservice, genai-backend-microservice, document-microservice, discovery-service]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Make gradlew executable
run: chmod +x ./gradlew
working-directory: services/backend/${{ matrix.service }}

- name: Run Unit Tests
run: ./gradlew test
working-directory: services/backend/${{ matrix.service }}

# Client (React) unit tests
test-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
working-directory: services/client
- run: npm run test -- --ci
working-directory: services/client

# GenAI Python unit tests (pytest)
test-genai:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r services/genai/requirements.txt
- name: Run Pytest
run: pytest tests/unit
working-directory: services/genai

# This job builds the server to ensure it compiles correctly with the generated code.
build-server:
runs-on: ubuntu-latest
strategy:
matrix:
service: [api-gateway, user-course-microservice, genai-backend-microservice, discovery-service]
service: [api-gateway, user-course-microservice, genai-backend-microservice, document-microservice, discovery-service]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -66,6 +116,45 @@ jobs:
java-version: '21'
distribution: 'temurin'

- name: Make gradlew executable
run: chmod +x ./gradlew
working-directory: services/backend/${{ matrix.service }}

- name: Build Server with Gradle
run: ./gradlew build -x test
working-directory: services/backend/${{ matrix.service }}

test-e2e:
runs-on: ubuntu-latest
env:
TUM_AET_LLM_API_KEY: ${{ secrets.TUM_AET_LLM_API_KEY }}
TUM_AET_LLM_API_BASE: ${{ secrets.TUM_AET_LLM_API_BASE }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'

- run: npm ci
- run: npx playwright install --with-deps

# Start your stack (adjust the compose file name if needed)
- name: Start services
run: docker compose -f docker-compose.yml up --build -d

# Wait for the frontend to be ready (adjust port if needed)
- name: Wait for frontend
run: npx wait-on http://localhost:3000

# Run your ordered Playwright tests
- run: npm run test:e2e:ordered:ci

# Optionally, show logs if tests fail
- name: Show docker logs on failure
if: failure()
run: docker compose logs

# Tear down
- name: Stop services
if: always()
run: docker compose down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,5 @@ server/bin
*.class
server/bin
node_modules/

test-results
62 changes: 62 additions & 0 deletions e2e/coursespace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';
import { login } from './helpers';

test.describe('Course Space', () => {
test('should show dashboard/coursespace page', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'My Course Spaces' })).toBeVisible();
});

test('should show a message if no course spaces exist', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'My Course Spaces' })).toBeVisible();
await expect(page.getByText("You haven't created any")).toBeVisible();
});

test('should allow user to create a new course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('button', { name: '✨ Create New Space' }).click();
await page.getByRole('textbox', { name: 'Title' }).click();
await page.getByRole('textbox', { name: 'Title' }).fill('Test Course');
await page.getByRole('textbox', { name: 'Description' }).click();
await page.getByRole('textbox', { name: 'Description' }).fill('some description');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.getByRole('link', { name: 'Test Course Delete course' })).toBeVisible();
});

test('should view details of a specific course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Edit' }).click();
await expect(page.getByRole('textbox', { name: 'Title' })).toHaveValue('Test Course');
await expect(page.getByRole('textbox', { name: 'Description' })).toHaveValue('some description');
await page.getByRole('button', { name: 'Cancel' }).click();
await page.getByRole('link', { name: 'Test Course Delete course' }).click();
await page.getByRole('heading', { name: 'Course Space: Test Course (' }).click();
});

test('should allow user to edit a course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Edit' }).click();
await page.getByRole('textbox', { name: 'Title' }).click();
await page.getByRole('textbox', { name: 'Title' }).fill('Test Course Update');
await page.getByRole('textbox', { name: 'Description' }).click();
await page.getByRole('textbox', { name: 'Description' }).fill('some description updated');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('link', { name: 'Test Course Update Delete course' })).toBeVisible();
});

test('should allow user to delete a course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
// Just check if the delete button exists somewhere on the page
// tailwind does not provide a way to click on it
const deleteBtn = page.locator('[aria-label="Delete course space"]');
await expect(deleteBtn.first()).toBeAttached({ timeout: 2000 });
});

});
77 changes: 77 additions & 0 deletions e2e/documents.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { test, expect } from '@playwright/test';
import path from 'path';
import { fileURLToPath } from 'url';
import { login } from './helpers';

test.describe('Documents', () => {
let courseSpaceName = 'Doc Test Multi';

test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('button', { name: '✨ Create New Space' }).click();
await page.getByRole('textbox', { name: 'Title' }).fill(courseSpaceName);
await page.getByRole('textbox', { name: 'Description' }).fill('doc test multi');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect(page.getByRole('link', { name: courseSpaceName })).toBeVisible();
await page.close();
});

test('should upload and list basic-text.pdf in the course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('link', { name: new RegExp(courseSpaceName) }).click();
await page.getByRole('button', { name: 'Documents' }).click();
const documentsTab = page.getByRole('button', { name: 'Documents' });
await documentsTab.scrollIntoViewIfNeeded();
await expect(documentsTab).toBeVisible();
await documentsTab.click();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const pdfPath = path.resolve(__dirname, 'fixtures/basic-text.pdf');
await page.getByTestId('pdf-input').setInputFiles(pdfPath);
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('Upload successful!')).toBeVisible();
await page.getByRole('cell', { name: 'basic-text.pdf' }).click();
});

test('should delete basic-text.pdf in the course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('link', { name: new RegExp(courseSpaceName) }).click();
await page.getByRole('button', { name: 'Documents' }).click();
const documentsTab = page.getByRole('button', { name: 'Documents' });
await documentsTab.scrollIntoViewIfNeeded();
await expect(documentsTab).toBeVisible();
await documentsTab.click();

// Delete the uploaded file and verify it is removed
await page.getByRole('button', { name: "Delete", exact: false }).click();
await expect(page.getByText('basic-text.pdf')).not.toBeVisible();
});

test('should upload and list large-pdf.pdf in the course space', async ({ page }) => {
await login(page, 'tester@user.com', '12345678ab');
await page.goto('/dashboard');
await page.getByRole('link', { name: new RegExp(courseSpaceName) }).click();
await page.getByRole('button', { name: 'Documents' }).click();
const documentsTab = page.getByRole('button', { name: 'Documents' });
await documentsTab.scrollIntoViewIfNeeded();
await expect(documentsTab).toBeVisible();
await documentsTab.click();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const largePdfPath = path.resolve(__dirname, 'fixtures/large-doc.pdf');
await page.getByTestId('pdf-input').setInputFiles(largePdfPath);
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('PDF cannot be processed. It')).toBeVisible();
await expect(page.getByRole('button', { name: 'Dismiss error' })).toBeVisible();
});


});


Binary file added e2e/fixtures/basic-text.pdf
Binary file not shown.
Binary file added e2e/fixtures/large-doc.pdf
Binary file not shown.
Binary file added e2e/fixtures/sample-report.pdf
Binary file not shown.
67 changes: 67 additions & 0 deletions e2e/flashcards.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect } from '@playwright/test';
import { login } from './helpers';

import path from 'path';
import { fileURLToPath } from 'url';

import { test } from '@playwright/test';

test.describe('Flashcards', () => {
test('should login, create a course space, upload a PDF, and open Flashcards tab', async ({ page }) => {
// Login as test user
await login(page, 'tester@user.com', '12345678ab');

// Go to dashboard
await page.goto('/dashboard');

// Create a new course space
await page.getByRole('button', { name: '✨ Create New Space' }).click();
await page.getByRole('textbox', { name: 'Title' }).fill('Flashcards Test Course');
await page.getByRole('textbox', { name: 'Description' }).fill('for flashcards e2e');
await page.getByRole('button', { name: 'Create', exact: true }).click();

// Wait for the new course space to appear and click it
await expect(page.getByRole('link', { name: 'Flashcards Test Course Delete course' })).toBeVisible();
await page.getByRole('link', { name: 'Flashcards Test Course Delete course' }).click();

// Go to the Documents tab first
await page.getByRole('button', { name: 'Documents' }).click();
const documentsTab = page.getByRole('button', { name: 'Documents' });
await documentsTab.scrollIntoViewIfNeeded();
await expect(documentsTab).toBeVisible();
await documentsTab.click();

// Upload the basic-text.pdf file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const pdfPath = path.resolve(__dirname, 'fixtures/basic-text.pdf');
await page.getByTestId('pdf-input').setInputFiles(pdfPath);
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('Upload successful!')).toBeVisible();

// Assert the file appears in the list (adjust selector as needed)
await expect(page.getByRole('cell', { name: 'basic-text.pdf' })).toBeVisible();

// Now go to the Flashcards tab (use the same approach as Documents)
const flashcardsTab = page.getByRole('button', { name: 'Flashcards' });
await flashcardsTab.scrollIntoViewIfNeeded();
await expect(flashcardsTab).toBeVisible();
await flashcardsTab.click();
await expect(page.getByRole('button', { name: '✨ Generate Flashcards' })).toBeVisible();
await expect(page.getByText('No flashcards generated yet.')).toBeVisible();
await expect(page.getByText('Click the button to start.')).toBeVisible();
await page.getByRole('button', { name: '✨ Generate Flashcards' }).click();

// Wait for the flashcard question to appear (longer timeout for slow generation)
const questionP = page.locator('div.flex.items-center.justify-center.cursor-pointer > p.text-2xl.font-semibold.text-gray-800');
await expect(questionP).toBeVisible({ timeout: 30000 });
await expect(await questionP.textContent()).not.toBe('');
await expect(page.getByRole('button', { name: 'Shuffle Deck' })).toBeVisible();
await page.getByRole('button', { name: 'Shuffle Deck' }).click();
await expect(page.getByRole('button', { name: 'Next →' })).toBeVisible();
await page.getByRole('button', { name: 'Next →' }).click();
const questionP2 = page.locator('div.flex.items-center.justify-center.cursor-pointer > p.text-2xl.font-semibold.text-gray-800');
await expect(questionP2).toBeVisible({ timeout: 30000 });
await expect(await questionP2.textContent()).not.toBe('');
});
});
10 changes: 10 additions & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// e2e/helpers.ts
import { Page, expect } from '@playwright/test';

export async function login(page: Page, email = 'test@user.com', password = '12345678') {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Email' }).fill(email);
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByRole('link', { name: 'LECture-bot' })).toBeVisible();
}
Loading
Loading