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
34 changes: 33 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
version: 2
updates:
# Frontend npm dependencies
- package-ecosystem: 'npm'
directory: '/'
directory: '/frontend'
schedule:
interval: 'weekly'
versioning-strategy: 'increase'
groups:
# Group minor/patch updates to reduce PR noise
minor-and-patch:
patterns:
- '*'
update-types:
- 'minor'
- 'patch'
ignore:
# Ignore major version updates for stability
- dependency-name: '*'
update-types: ['version-update:semver-major']

# Backend Go dependencies
- package-ecosystem: 'gomod'
directory: '/backend'
schedule:
interval: 'weekly'
versioning-strategy: 'increase'
groups:
go-minor-and-patch:
patterns:
- '*'
update-types:
- 'minor'
- 'patch'

# GitHub Actions dependencies
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
groups:
actions:
patterns:
- '*'
14 changes: 14 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
- frontend/tsconfig*.json
- frontend/prettier.config.js
- frontend/postcss.config.js
- frontend/playwright.config.ts

# Add 'tests' label for test-related files
'tests':
- frontend/e2e/**
- frontend/tests/**
- frontend/playwright.config.ts
- frontend/playwright.global-setup.ts
- frontend/jest.config.ts
- frontend/jest.setup.ts

# Add 'documentation' label for documentation-related files
'documentation':
Expand All @@ -44,6 +54,10 @@
- backend/Dockerfile
- backend/.dockerignore

# Add 'helm' label for Helm chart files
'helm':
- chart/**

# Add 'other' label as a fallback for any other files
'other':
- '*'
92 changes: 66 additions & 26 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ on:

jobs:
test:
timeout-minutes: 60
timeout-minutes: 30
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# Run tests on multiple browsers
# Run tests on multiple browsers with sharding for parallelization
browser: [chromium, firefox, webkit]
shard: [1/4, 2/4, 3/4, 4/4]

env:
WARMUP_BROWSER: ${{ matrix.browser }}
# Use 2 workers for stability
PLAYWRIGHT_WORKERS: 2

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -50,39 +54,75 @@ jobs:

- name: Install Playwright Browsers
working-directory: ./frontend
run: npx playwright install --with-deps ${{ matrix.browser }}

- name: Verify Playwright installation
working-directory: ./frontend
run: npx playwright --version

- name: List available tests
working-directory: ./frontend
run: npx playwright test --list --project=${{ matrix.browser }} > /tmp/playwright_list.txt && head -5 /tmp/playwright_list.txt && rm /tmp/playwright_list.txt

- name: Build frontend
working-directory: ./frontend
env:
VITE_USE_MSW: 'true'
run: npm run build
run: |
npx playwright install --with-deps ${{ matrix.browser }}
# Ensure all system dependencies are installed for webkit
if [ "${{ matrix.browser }}" = "webkit" ]; then
sudo apt-get update
sudo apt-get install -y libwoff1 libopus0 libwebpdemux2 libharfbuzz-icu0 libenchant-2-2 libsecret-1-0 libhyphen0 libmanette-0.2-0 libflite1 libgles2 gstreamer1.0-libav
fi

- name: Run Playwright tests
working-directory: ./frontend
run: npx playwright test --project=${{ matrix.browser }}
run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
env:
CI: true
VITE_USE_MSW: 'true'

- uses: actions/upload-artifact@v4
- name: Upload blob report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.browser }}
path: frontend/playwright-report/
retention-days: 7

- uses: actions/upload-artifact@v4
if: always()
name: blob-report-${{ matrix.browser }}-${{ strategy.job-index }}
path: frontend/blob-report/
retention-days: 1

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.browser }}
name: test-results-${{ matrix.browser }}-${{ strategy.job-index }}
path: frontend/test-results/
retention-days: 7

# Merge all sharded reports into a single HTML report per browser
merge-reports:
name: Merge Reports
needs: test
runs-on: ubuntu-latest
if: always()

strategy:
matrix:
browser: [chromium, firefox, webkit]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
working-directory: ./frontend
run: npm ci

- name: Download blob reports for ${{ matrix.browser }}
uses: actions/download-artifact@v4
with:
pattern: blob-report-${{ matrix.browser }}-*
path: frontend/all-blob-reports
merge-multiple: true

- name: Merge reports
working-directory: ./frontend
run: npx playwright merge-reports --reporter html ./all-blob-reports

- name: Upload merged HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.browser }}
path: frontend/playwright-report/
retention-days: 14
7 changes: 4 additions & 3 deletions frontend/e2e/CommandPalette.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ test.describe('Command Palette', () => {
// Login first to access the command palette
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });

// Wait for login page to be ready
await page.waitForSelector('input[type="text"], input[name="username"]', { timeout: 10000 });
// Wait for login form to be ready using role-based locator (auto-retries)
const usernameInput = page.getByRole('textbox', { name: 'Username' });
await expect(usernameInput).toBeVisible({ timeout: 15000 });

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
await usernameInput.fill('admin');
await page.getByRole('textbox', { name: 'Password' }).fill('admin');
await page.getByRole('button', { name: /Sign In|Sign In to/i }).click();

Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/Dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ test.describe('Dashboard Page', () => {
const firstCluster = page.getByRole('heading', { name: 'cluster1' }).first();
await firstCluster.click();

await expect(page.locator('[role="dialog"], .modal')).toBeVisible({ timeout: 2000 });
await expect(page.locator('[role="dialog"], .modal')).toBeVisible({ timeout: 10000 });

await page.keyboard.press('Escape');
});
Expand Down
53 changes: 30 additions & 23 deletions frontend/e2e/Login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,37 +218,44 @@ test.describe('Login Page', () => {
await loginPage.fillPassword('wrongpassword');
await loginPage.clickSignIn();

// Wait for loading toast or status indicator to disappear, or error toast/alert to appear
await Promise.race([
page
.locator('.toast-loading, [role="status"]')
// Wait for error response - either an error message appears or we stay on login page
// The app uses react-hot-toast which renders in a div with specific structure
const errorMessageLocator = page.locator('text=/Invalid|Error|failed|incorrect/i');
const toastLocator = page.locator(
'[role="alert"], [data-sonner-toast], .toast-error, [class*="toast"]'
);

// Try to find error indication with increased timeout
let errorFound = false;
try {
// First wait for the sign-in button to be enabled again (indicates request completed)
await page
.getByRole('button', { name: /Sign In/i })
.waitFor({ state: 'visible', timeout: 10000 });

// Then check for error message or toast
const hasErrorMessage = await errorMessageLocator
.first()
.waitFor({ state: 'hidden', timeout: 3000 }),
page
.locator('[role="alert"], .toast-error')
.isVisible()
.catch(() => false);
const hasToast = await toastLocator
.first()
.waitFor({ state: 'visible', timeout: 3000 }),
]);

// Look for any text containing error messages
const errorTexts = ['Invalid username or password'];
.isVisible()
.catch(() => false);

let errorFound = false;
for (const errorText of errorTexts) {
try {
const errorElement = page.locator(`text=${errorText}`);
await errorElement.waitFor({ timeout: 2000 });
await expect(errorElement).toBeVisible();
if (hasErrorMessage || hasToast) {
errorFound = true;
break;
} catch {
// Continue to next error text
}
} catch {
// Request may still be processing
}

// If no specific error text found, check if we're still on login page (which indicates error)
// If no specific error found, verify we're still on login page (which indicates login failed)
if (!errorFound) {
await expect(page).toHaveURL(/login/);
await expect(page).toHaveURL(/login/, { timeout: 5000 });
}

// Final verification: we should still be on login page after failed login
await expect(page).toHaveURL(/login/);
});
});
37 changes: 4 additions & 33 deletions frontend/e2e/Navbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ test.describe('Navbar (Header)', () => {
// Login first to access the header
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });

// Wait for login page to be ready
await page.waitForSelector('input[type="text"], input[name="username"]', { timeout: 10000 });
// Wait for login form to be ready using role-based locator (auto-retries)
const usernameInput = page.getByRole('textbox', { name: 'Username' });
await expect(usernameInput).toBeVisible({ timeout: 15000 });

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
await usernameInput.fill('admin');
await page.getByRole('textbox', { name: 'Password' }).fill('admin');
await page.getByRole('button', { name: /Sign In|Sign In to/i }).click();

Expand Down Expand Up @@ -259,35 +260,5 @@ test.describe('Navbar (Header)', () => {
}
}
});

test('navbar has consistent styling', async ({ page }) => {
const header = page.locator('header');

// Check if header has some styling applied (background, border, or shadow)
const styles = await header.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
backgroundColor: computed.backgroundColor,
borderBottom: computed.borderBottom,
boxShadow: computed.boxShadow,
position: computed.position,
};
});

// Header should have some styling - at least background color or position
const hasBackground =
styles.backgroundColor &&
styles.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
styles.backgroundColor !== 'transparent';
const hasBorder =
styles.borderBottom &&
styles.borderBottom !== 'none' &&
styles.borderBottom !== '0px none rgba(0, 0, 0, 0)';
const hasShadow = styles.boxShadow && styles.boxShadow !== 'none';
const isPositioned = styles.position === 'fixed' || styles.position === 'sticky';

// Should have at least one styling feature
expect(hasBackground || hasBorder || hasShadow || isPositioned).toBe(true);
});
});
});
2 changes: 1 addition & 1 deletion frontend/e2e/ObjectExplorerViewModes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test.describe('Object Explorer - View Modes and Pagination', () => {
let objectExplorerPage: ObjectExplorerPage;
let mswHelper: MSWHelper;

test.describe.configure({ timeout: 60000 });
test.describe.configure({ timeout: 120000 });
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
objectExplorerPage = new ObjectExplorerPage(page);
Expand Down
7 changes: 4 additions & 3 deletions frontend/e2e/ProfileSection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ test.describe('Profile Section', () => {
// Login first to access the profile section
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });

// Wait for login page to be ready
await page.waitForSelector('input[type="text"], input[name="username"]', { timeout: 10000 });
// Wait for login form to be ready using role-based locator (auto-retries)
const usernameInput = page.getByRole('textbox', { name: 'Username' });
await expect(usernameInput).toBeVisible({ timeout: 15000 });

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
await usernameInput.fill('admin');
await page.getByRole('textbox', { name: 'Password' }).fill('admin');
await page.getByRole('button', { name: /Sign In|Sign In to/i }).click();

Expand Down
7 changes: 4 additions & 3 deletions frontend/e2e/ThemeToggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ test.describe('Theme Toggle Button', () => {
// Login first to access the header
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });

// Wait for login page to be ready
await page.waitForSelector('input[type="text"], input[name="username"]', { timeout: 10000 });
// Wait for login form to be ready using role-based locator (auto-retries)
const usernameInput = page.getByRole('textbox', { name: 'Username' });
await expect(usernameInput).toBeVisible({ timeout: 15000 });

await page.getByRole('textbox', { name: 'Username' }).fill('admin');
await usernameInput.fill('admin');
await page.getByRole('textbox', { name: 'Password' }).fill('admin');
await page.getByRole('button', { name: /Sign In|Sign In to/i }).click();

Expand Down
7 changes: 4 additions & 3 deletions frontend/e2e/pages/LoginPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,14 @@ export class LoginPage extends BasePage {
const options = Array.from(document.querySelectorAll('[role="option"]'));
return options.some(opt => opt.textContent?.includes(text));
},

languageText,
{ timeout: 2000 }
{ timeout: 5000 }
);

// Use a more reliable click approach - wait for element to be actionable
await languageOption.waitFor({ state: 'attached', timeout: 2000 });
await expect(languageOption).toBeEnabled({ timeout: 2000 });
await languageOption.waitFor({ state: 'attached', timeout: 5000 });
await expect(languageOption).toBeEnabled({ timeout: 5000 });

// Click with retry handling
try {
Expand Down
Loading
Loading