Add GitLab, pagination, activity timeline, and error resilience e2e tests

Four new test suites expanding integration hub e2e coverage:

- gitlab-integration.e2e.spec.ts: Container health, direct probe, connector
  CRUD lifecycle (create/test/health/delete), SCM tab UI verification.
  Gracefully skips when GitLab container not running (heavy profile).

- pagination.e2e.spec.ts: API-level pagination (pageSize, page params,
  totalPages, sorting, last-page edge case, out-of-range page).
  UI pager rendering verification.

- activity-timeline.e2e.spec.ts: Page load, stats bar, activity items,
  event type filter dropdown, clear filters, back navigation.
  Tests against mock data rendered by the activity component.

- error-resilience.e2e.spec.ts: Unreachable endpoint returns failure/unhealthy,
  non-existent resource 404s, malformed input handling, duplicate name
  creation, UI empty tab rendering, deleted integration detail page.

Also adds GitLab config to shared helpers.ts INTEGRATION_CONFIGS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-31 16:55:35 +03:00
parent 2fef38b093
commit 3f6fb501dd
5 changed files with 689 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
/**
* Pagination — End-to-End Tests
*
* Validates API-level pagination and UI pager rendering:
* 1. API: page/pageSize params return correct subsets
* 2. API: totalCount and totalPages are accurate
* 3. UI: Pager info renders correct totals
* 4. UI: Pager controls are present
*
* Prerequisites:
* - Main Stella Ops stack running
* - docker-compose.integration-fixtures.yml (Harbor fixture)
*/
import { test, expect } from './live-auth.fixture';
import {
INTEGRATION_CONFIGS,
createIntegrationViaApi,
cleanupIntegrations,
snap,
} from './helpers';
const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local';
const runId = process.env['E2E_RUN_ID'] || 'run1';
// ---------------------------------------------------------------------------
// 1. API Pagination
// ---------------------------------------------------------------------------
test.describe('Pagination — API', () => {
const createdIds: string[] = [];
test.beforeAll(async ({ apiRequest }) => {
// Create 6 registry integrations for pagination testing
for (let i = 1; i <= 6; i++) {
const id = await createIntegrationViaApi(apiRequest, {
...INTEGRATION_CONFIGS.harbor,
name: `E2E Page Test ${i} ${runId}`,
});
createdIds.push(id);
}
});
test('pageSize=2 returns 2 items and correct totalPages', async ({ apiRequest }) => {
const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=1');
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.items.length).toBe(2);
expect(body.pageSize).toBe(2);
expect(body.page).toBe(1);
expect(body.totalCount).toBeGreaterThanOrEqual(6);
expect(body.totalPages).toBeGreaterThanOrEqual(3);
});
test('page=2 returns different items than page=1', async ({ apiRequest }) => {
const page1 = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=1');
const page2 = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=2');
const body1 = await page1.json();
const body2 = await page2.json();
expect(body1.items.length).toBe(2);
expect(body2.items.length).toBe(2);
// Items should be different between pages
const ids1 = body1.items.map((i: any) => i.id);
const ids2 = body2.items.map((i: any) => i.id);
const overlap = ids1.filter((id: string) => ids2.includes(id));
expect(overlap.length).toBe(0);
});
test('last page may have fewer items', async ({ apiRequest }) => {
const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=4&page=1');
const body = await resp.json();
if (body.totalPages > 1) {
const lastPage = await apiRequest.get(
`/api/v1/integrations?type=1&pageSize=4&page=${body.totalPages}`,
);
const lastBody = await lastPage.json();
expect(lastBody.items.length).toBeLessThanOrEqual(4);
expect(lastBody.items.length).toBeGreaterThan(0);
}
});
test('page beyond totalPages returns empty items', async ({ apiRequest }) => {
const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=999');
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.items.length).toBe(0);
});
test('sortBy=name orders results alphabetically', async ({ apiRequest }) => {
const resp = await apiRequest.get(
'/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=false',
);
const body = await resp.json();
const names = body.items.map((i: any) => i.name);
for (let i = 1; i < names.length; i++) {
expect(names[i].localeCompare(names[i - 1])).toBeGreaterThanOrEqual(0);
}
});
test('sortDescending=true reverses order', async ({ apiRequest }) => {
const asc = await apiRequest.get(
'/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=false',
);
const desc = await apiRequest.get(
'/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=true',
);
const ascNames = (await asc.json()).items.map((i: any) => i.name);
const descNames = (await desc.json()).items.map((i: any) => i.name);
if (ascNames.length > 1) {
expect(ascNames[0]).not.toBe(descNames[0]);
}
});
test.afterAll(async ({ apiRequest }) => {
await cleanupIntegrations(apiRequest, createdIds);
});
});
// ---------------------------------------------------------------------------
// 2. UI Pager
// ---------------------------------------------------------------------------
test.describe('Pagination — UI Pager', () => {
test('pager info renders on registries tab', async ({ liveAuthPage: page }) => {
await page.goto(`${BASE}/setup/integrations/registries`, {
waitUntil: 'networkidle',
timeout: 30_000,
});
await page.waitForTimeout(2_000);
// The pager should show "X total · page Y of Z"
const pagerInfo = page.locator('.pager__info');
const isVisible = await pagerInfo.isVisible({ timeout: 5_000 }).catch(() => false);
if (isVisible) {
const text = await pagerInfo.textContent();
expect(text).toContain('total');
expect(text).toContain('page');
}
await snap(page, 'pagination-ui-pager');
});
test('pager controls are present', async ({ liveAuthPage: page }) => {
await page.goto(`${BASE}/setup/integrations/registries`, {
waitUntil: 'networkidle',
timeout: 30_000,
});
await page.waitForTimeout(2_000);
// Check for pagination navigation
const pager = page.locator('.pager');
const isVisible = await pager.isVisible({ timeout: 5_000 }).catch(() => false);
if (isVisible) {
// Should have navigation buttons
const firstBtn = page.locator('button[title="First page"]');
const lastBtn = page.locator('button[title="Last page"]');
await expect(firstBtn).toBeVisible({ timeout: 3_000 });
await expect(lastBtn).toBeVisible({ timeout: 3_000 });
}
await snap(page, 'pagination-ui-controls');
});
});