Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"cSpell.words": [
"animateonscroll",
"aswf",
"AUTHELIA",
"autojoin",
"autorestart",
"cloudops",
"confirmdialog",
Expand Down
4 changes: 4 additions & 0 deletions apps/lfx-pcc/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ SUPABASE_URL=https://your-project.supabase.co
POSTGRES_API_KEY=your-supabase-anon-key
SUPABASE_STORAGE_BUCKET=your-supabase-bucket-name

# NATS Configuration
# Internal k8s service DNS for NATS cluster
NATS_URL=nats://lfx-platform-nats.lfx.svc.cluster.local:4222

# E2E Test Configuration (Optional)
# Test user credentials for automated testing
TEST_USERNAME=your-test-username
Expand Down
9 changes: 9 additions & 0 deletions apps/lfx-pcc/e2e/fixtures/mock-data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

/**
* Central exports for all mock data
* Provides easy access to project mock data for Playwright tests
*/

export * from './projects.mock';
120 changes: 120 additions & 0 deletions apps/lfx-pcc/e2e/fixtures/mock-data/projects.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { Project } from '@lfx-pcc/shared/interfaces';

/**
* Mock project data for Playwright tests
* Organized by project slug for easy lookup
*/
export const mockProjects: Record<string, Project> = {
aswf: {
uid: 'a09f1234-f567-4abc-b890-1234567890ab',
slug: 'aswf',
name: 'Academy Software Foundation (ASWF)',
description:
"The mission of the Academy Software Foundation (ASWF) is to increase the quality and quantity of contributions to the content creation industry's open source software base; to provide a neutral forum to coordinate cross-project efforts; to provide a common build and test infrastructure; and to provide individuals and organizations a clear path to participation in advancing our open source ecosystem.",
public: true,
parent_uid: '',
stage: 'graduated',
category: 'foundation',
funding_model: ['member-funded'],
charter_url: 'https://github.com/AcademySoftwareFoundation/tac/blob/main/charter.md',
legal_entity_type: 'directed-fund',
legal_entity_name: 'Academy Software Foundation',
legal_parent_uid: '',
autojoin_enabled: false,
formation_date: '2018-08-10T00:00:00Z',
logo_url: 'https://artwork.aswf.io/projects/aswf/horizontal/color/aswf-horizontal-color.png',
repository_url: 'https://github.com/AcademySoftwareFoundation',
website_url: 'https://www.aswf.io/',
created_at: '2018-08-10T00:00:00Z',
updated_at: new Date().toISOString(),
committees_count: 5,
meetings_count: 23,
mailing_list_count: 8,
},
cncf: {
uid: 'b09f1234-f567-4abc-b890-1234567890bc',
slug: 'cncf',
name: 'Cloud Native Computing Foundation',
description:
"The Cloud Native Computing Foundation (CNCF) hosts critical components of the global technology infrastructure. CNCF brings together the world's top developers, end users, and vendors and runs the largest open source developer conferences.",
public: true,
parent_uid: '',
stage: 'graduated',
category: 'foundation',
funding_model: ['member-funded'],
charter_url: 'https://github.com/cncf/foundation/blob/master/charter.md',
legal_entity_type: 'directed-fund',
legal_entity_name: 'Cloud Native Computing Foundation',
legal_parent_uid: '',
autojoin_enabled: false,
formation_date: '2015-12-11T00:00:00Z',
logo_url: 'https://landscape.cncf.io/logos/cncf-color.svg',
repository_url: 'https://github.com/cncf',
website_url: 'https://www.cncf.io/',
created_at: '2015-12-11T00:00:00Z',
updated_at: new Date().toISOString(),
committees_count: 12,
meetings_count: 156,
mailing_list_count: 25,
},
kubernetes: {
uid: 'c09f1234-f567-4abc-b890-1234567890cd',
slug: 'kubernetes',
name: 'Kubernetes',
description: 'Kubernetes is an open-source system for automating deployment, scaling, and management of containerized applications.',
public: true,
parent_uid: 'b09f1234-f567-4abc-b890-1234567890bc', // CNCF parent
stage: 'graduated',
category: 'project',
funding_model: ['foundation-funded'],
charter_url: 'https://github.com/kubernetes/community/blob/master/governance.md',
legal_entity_type: 'project',
legal_entity_name: 'Kubernetes',
legal_parent_uid: 'b09f1234-f567-4abc-b890-1234567890bc',
autojoin_enabled: true,
formation_date: '2014-06-07T00:00:00Z',
logo_url: 'https://kubernetes.io/images/kubernetes-horizontal-color.png',
repository_url: 'https://github.com/kubernetes/kubernetes',
website_url: 'https://kubernetes.io/',
created_at: '2014-06-07T00:00:00Z',
updated_at: new Date().toISOString(),
committees_count: 8,
meetings_count: 89,
mailing_list_count: 15,
},
};

/**
* Get mock project by slug
* @param slug - Project slug to look up
* @returns Project object or undefined if not found
*/
export function getMockProject(slug: string): Project | undefined {
return mockProjects[slug];
}

/**
* Get all mock projects as an array
* @returns Array of all mock projects
*/
export function getAllMockProjects(): Project[] {
return Object.values(mockProjects);
}

/**
* Search mock projects by name or description
* @param query - Search query string
* @returns Array of matching projects
*/
export function searchMockProjects(query: string): Project[] {
const searchTerm = query.toLowerCase();
return getAllMockProjects().filter(
(project) =>
project.name.toLowerCase().includes(searchTerm) ||
project.description.toLowerCase().includes(searchTerm) ||
project.slug.toLowerCase().includes(searchTerm)
);
}
54 changes: 54 additions & 0 deletions apps/lfx-pcc/e2e/helpers/api-mock.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

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

import { getMockProject } from '../fixtures/mock-data';

/**
* Helper class for mocking the project slug endpoint in Playwright tests
* Only handles /api/projects/:slug endpoint that depends on NATS
*/
export class ApiMockHelper {
/**
* Setup mock for the project slug endpoint only
* @param page - Playwright page instance
*/
static async setupProjectSlugMock(page: Page): Promise<void> {
// Mock individual project by slug endpoint (/api/projects/:slug)
await page.route('**/api/projects/*', async (route) => {
const url = route.request().url();

// Skip other endpoints - only handle direct slug requests
if (url.includes('/search') || url.includes('/recent-activity')) {
await route.continue();
return;
}

const pathSegments = url.split('/');
const slug = pathSegments[pathSegments.length - 1].split('?')[0]; // Remove query params

console.log(`[Mock] Intercepting project request for slug: "${slug}"`);

const project = getMockProject(slug);

if (!project) {
await route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({
error: 'Project not found',
code: 'PROJECT_NOT_FOUND',
}),
});
return;
}

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(project),
});
});
}
}
16 changes: 12 additions & 4 deletions apps/lfx-pcc/e2e/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

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

import { ApiMockHelper } from './helpers/api-mock.helper';

test.describe('Homepage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
Expand Down Expand Up @@ -112,6 +114,9 @@ test.describe('Homepage', () => {
});

test('should filter projects when searching', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand Down Expand Up @@ -144,6 +149,9 @@ test.describe('Homepage', () => {
});

test('should clear search and show all projects', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand All @@ -167,6 +175,9 @@ test.describe('Homepage', () => {
});

test('should navigate to project detail when clicking a project card', async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Wait for project cards to appear
await expect(page.getByTestId('project-card').first()).toBeVisible({ timeout: 10000 });

Expand All @@ -176,17 +187,14 @@ test.describe('Homepage', () => {
.filter({ has: page.getByTestId('project-title').filter({ hasText: 'Cloud Native Computing Foundation' }) });
await expect(cncfCard).toBeVisible();

// Get the project name to verify navigation
const projectName = await cncfCard.getByTestId('project-title').innerText();

// Click the project card
await cncfCard.click();

// Wait for navigation by checking URL change
await expect(page).toHaveURL(/\/project\/[\w-]+$/, { timeout: 10000 });

// Verify project detail page elements - use heading that contains the project name
await expect(page.getByRole('heading', { level: 1 }).filter({ hasText: projectName })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 }).filter({ hasText: 'Cloud Native Computing Foundation' })).toBeVisible();
await expect(page.getByRole('link', { name: 'All Projects' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Meetings' })).toBeVisible();
Expand Down
5 changes: 5 additions & 0 deletions apps/lfx-pcc/e2e/project-dashboard-robust.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

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

import { ApiMockHelper } from './helpers/api-mock.helper';

test.describe('Project Dashboard - Robust Tests', () => {
test.beforeEach(async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Navigate to homepage and search for a project
await page.goto('/', { waitUntil: 'domcontentloaded' });

Expand Down
16 changes: 11 additions & 5 deletions apps/lfx-pcc/e2e/project-dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

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

import { ApiMockHelper } from './helpers/api-mock.helper';

test.describe('Project Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Setup API mocks before navigation
await ApiMockHelper.setupProjectSlugMock(page);

// Navigate to homepage and search for a project
await page.goto('/', { waitUntil: 'domcontentloaded' });

Expand Down Expand Up @@ -68,11 +73,12 @@ test.describe('Project Dashboard', () => {
});

test('should display all navigation tabs', async ({ page }) => {
await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Meetings' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Committees' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Mailing Lists' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible();
console.log(await page.getByTestId('menu-item').allInnerTexts());
await expect(page.getByTestId('menu-item').filter({ hasText: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Meetings' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Committees' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Mailing Lists' })).toBeVisible();
await expect(page.getByTestId('menu-item').filter({ hasText: 'Settings' })).toBeVisible();
});
});

Expand Down
1 change: 1 addition & 0 deletions apps/lfx-pcc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dotenv": "^17.2.1",
"express": "^4.18.2",
"express-openid-connect": "^2.19.2",
"nats": "^2.29.3",
"ngx-cookie-service-ssr": "^19.1.2",
"pino-http": "^10.5.0",
"primeng": "^19.1.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
<div class="flex items-center gap-8 mb-8 flex-wrap">
@for (metric of metrics(); track metric.label) {
<div class="flex flex-col">
<div class="flex items-center gap-2 mb-1">
<div class="flex items-center gap-2 mb-1" data-testid="metric-label">
<i [class]="metric.icon" class="text-sm"></i>
<span class="text-sm text-gray-600">{{ metric.label }}</span>
</div>
<div class="text-2xl font-normal text-gray-900 font-display">{{ metric.value }}</div>
<div class="text-2xl font-normal text-gray-900 font-display" data-testid="metric-value">{{ metric.value }}</div>
</div>
}
</div>
Expand All @@ -62,6 +62,7 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
[routerLink]="menu.routerLink"
class="pill"
routerLinkActive="bg-blue-50 text-blue-600 border-0"
data-testid="menu-item"
[routerLinkActiveOptions]="menu.routerLinkActiveOptions">
@if (menu.icon) {
<i [class]="menu.icon"></i>
Expand All @@ -74,9 +75,10 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
routerLink="/project/{{ projectSlug() }}/settings"
class="pill"
routerLinkActive="bg-blue-50 text-blue-600 border-0 block md:hidden"
data-testid="mobile-menu-item"
[routerLinkActiveOptions]="{ exact: true }">
<i class="fa-light fa-gear"></i>
Settings
<span>Settings</span>
</a>
</div>
</div>
Expand All @@ -85,9 +87,10 @@ <h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
routerLink="/project/{{ projectSlug() }}/settings"
class="pill"
routerLinkActive="bg-blue-50 text-blue-600 border-0"
data-testid="menu-item"
[routerLinkActiveOptions]="{ exact: true }">
<i class="fa-light fa-gear"></i>
Settings
<span>Settings</span>
</a>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class ProjectComponent {
if (!project?.slug) {
return of([]);
}
return this.activityService.getRecentActivitiesByProject(project.slug, 5).pipe(
return this.activityService.getRecentActivitiesByProject(project.uid, 5).pipe(
catchError((error) => {
console.error('Error loading recent activities:', error);
return of([]);
Expand Down
4 changes: 2 additions & 2 deletions apps/lfx-pcc/src/app/shared/services/activity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ export class ActivityService {
/**
* Get recent activity for a specific project
*/
public getRecentActivitiesByProject(projectSlug: string, limit: number = 10): Observable<RecentActivity[]> {
public getRecentActivitiesByProject(projectUid: string, limit: number = 10): Observable<RecentActivity[]> {
let params = new HttpParams();

if (limit) {
params = params.set('limit', limit.toString());
}

return this.http.get<RecentActivity[]>(`/api/projects/${projectSlug}/recent-activity`, { params }).pipe(
return this.http.get<RecentActivity[]>(`/api/projects/${projectUid}/recent-activity`, { params }).pipe(
catchError((error) => {
console.error('Failed to load recent activities:', error);
return of([]);
Expand Down
Loading
Loading