Skip to content

Commit 37db01b

Browse files
Merge branch 'main' into feat/LFXV2-2245
2 parents 1486fff + c2bb71e commit 37db01b

17 files changed

Lines changed: 2097 additions & 46 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { expect, Page, test } from '@playwright/test';
5+
6+
const ORG_PROJECTS_URL = '/org/projects';
7+
const DATA_LOAD_TIMEOUT = 30_000;
8+
const MOCK_ACCOUNT_ID = '0014100000Te2QjAAJ';
9+
const MOCK_UID = '4c46585f-878c-8285-b2e9-2dbfc38ddd9b';
10+
11+
test.setTimeout(120_000);
12+
13+
function skipWhenAuthMissing(page: Page): void {
14+
try {
15+
const { hostname } = new URL(page.url());
16+
if (hostname === 'auth0.com' || hostname.endsWith('.auth0.com')) {
17+
test.skip(true, 'TEST_USERNAME / TEST_PASSWORD not configured — see global-setup.ts');
18+
}
19+
} catch {
20+
// Let malformed URLs fail naturally.
21+
}
22+
}
23+
24+
// The Projects page renders from a client-side demo-data service (no projects API yet), so the only
25+
// stub needed is the personas endpoint to give the org context an account to select.
26+
async function stubOrgContext(page: Page): Promise<void> {
27+
await page.route('**/api/user/personas*', (route) =>
28+
route.fulfill({
29+
status: 200,
30+
contentType: 'application/json',
31+
body: JSON.stringify({
32+
personas: ['contributor'],
33+
personaProjects: {},
34+
projects: [],
35+
organizations: [
36+
{
37+
accountId: MOCK_ACCOUNT_ID,
38+
accountName: 'Red Hat LLC',
39+
accountSlug: 'red-hat-llc',
40+
membershipTier: '',
41+
uid: MOCK_UID,
42+
},
43+
],
44+
isRootWriter: false,
45+
}),
46+
})
47+
);
48+
}
49+
50+
async function gotoOrgProjectsPage(page: Page): Promise<void> {
51+
await stubOrgContext(page);
52+
await page.goto('/', { waitUntil: 'domcontentloaded' });
53+
skipWhenAuthMissing(page);
54+
await page.reload({ waitUntil: 'domcontentloaded' });
55+
await page.goto(ORG_PROJECTS_URL, { waitUntil: 'domcontentloaded' });
56+
skipWhenAuthMissing(page);
57+
await expect(page).not.toHaveURL(/auth0\.com/);
58+
59+
if (!page.url().includes('/org/projects')) {
60+
test.skip(true, 'org-lens-enabled flag appears off — /org/projects redirected away');
61+
}
62+
}
63+
64+
test.describe('Org Projects', () => {
65+
test('renders the projects table with demo data', async ({ page }) => {
66+
await gotoOrgProjectsPage(page);
67+
68+
await expect(page.getByTestId('org-projects-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
69+
await expect(page.getByTestId('org-projects-table')).toBeVisible();
70+
// Demo dataset always includes Kubernetes (Leading / Leading).
71+
const kubernetesRow = page.getByTestId('org-projects-row-kubernetes');
72+
await expect(kubernetesRow).toBeVisible();
73+
await expect(kubernetesRow.getByText('Leading').first()).toBeVisible();
74+
await expect(page.getByTestId('org-projects-export-csv')).toBeVisible();
75+
});
76+
77+
test('sorts by project name from the column header (persists to the URL)', async ({ page }) => {
78+
await gotoOrgProjectsPage(page);
79+
await expect(page.getByTestId('org-projects-table')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
80+
81+
await page.getByTestId('org-projects-sort-name').click();
82+
await expect(page).toHaveURL(/[?&]sort=name/);
83+
});
84+
85+
test('opens the workspace dropdown and the add-workspace dialog', async ({ page }) => {
86+
await gotoOrgProjectsPage(page);
87+
await expect(page.getByTestId('org-projects-page')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
88+
89+
await page.getByTestId('org-projects-workspace-trigger').click();
90+
// Each company is seeded with only the default workspace.
91+
await expect(page.getByTestId('org-projects-workspace-option-all-activities')).toBeVisible();
92+
93+
await page.getByTestId('org-projects-add-workspace').click();
94+
await expect(page.getByTestId('org-projects-workspace-dialog')).toBeVisible();
95+
await expect(page.getByTestId('org-projects-workspace-save')).toBeVisible();
96+
});
97+
98+
test('reveals the LFX Insights health detail on hover', async ({ page }) => {
99+
await gotoOrgProjectsPage(page);
100+
await expect(page.getByTestId('org-projects-row-kubernetes')).toBeVisible({ timeout: DATA_LOAD_TIMEOUT });
101+
102+
await page.getByTestId('org-projects-health-kubernetes').hover();
103+
const popover = page.getByTestId('org-projects-health-popover');
104+
await expect(popover).toBeVisible();
105+
await expect(popover.getByRole('link', { name: /LFX Insights/ })).toBeVisible();
106+
});
107+
});

apps/lfx-one/src/app/app.routes.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,9 @@ export const routes: Routes = [
109109
import('./modules/dashboards/org/org-membership-detail/org-membership-detail.component').then((m) => m.OrgMembershipDetailComponent),
110110
},
111111
{
112-
// INFO: Future Epic implementation — the Projects page is hidden; deep links
113-
// fall back to the org overview until the org-projects drilldown is built.
114112
path: 'projects',
115-
redirectTo: 'overview',
116-
pathMatch: 'full',
113+
data: { lens: 'org', title: 'Projects', description: 'Projects your organization participates in.', icon: 'fa-light fa-folder' },
114+
loadComponent: () => import('./modules/dashboards/org/org-projects/org-projects.component').then((m) => m.OrgProjectsComponent),
117115
},
118116
{
119117
// INFO: Future Epic implementation — the ROI page is hidden; deep links fall

apps/lfx-one/src/app/modules/dashboards/marketing-impact/components/social-accounts-tab/social-accounts-tab.component.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,17 @@ export class SocialAccountsTabComponent {
218218

219219
const expanded = this.expandedPlatforms();
220220

221+
const currentYear = new Date().getUTCFullYear();
222+
const currentMonth = new Date().getUTCMonth();
223+
221224
return data.platforms.map((p) => {
222225
const rowsByMonth = new Map(p.months.map((m) => [m.month, m]));
223-
const latestRow = p.months.length > 0 ? p.months.reduce((latest, row) => (row.month > latest.month ? row : latest)) : null;
226+
const maxMonth = data.year === currentYear ? currentMonth : 11;
227+
const maxMonthStr = `${data.year}-${String(maxMonth + 1).padStart(2, '0')}`;
228+
const filteredMonths = p.months.filter((m) => m.month <= maxMonthStr);
229+
const latestRow = filteredMonths.length > 0 ? filteredMonths.reduce((latest, row) => (row.month > latest.month ? row : latest)) : null;
224230

225-
const allMonths: SocialMonthlyRow[] = MONTH_NAMES.map((name, i) => {
231+
const allMonths: SocialMonthlyRow[] = MONTH_NAMES.slice(0, maxMonth + 1).map((name, i) => {
226232
const monthStr = `${data.year}-${String(i + 1).padStart(2, '0')}`;
227233
const row = rowsByMonth.get(monthStr);
228234
if (!row) {

0 commit comments

Comments
 (0)