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:
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user