diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 5cc5d2f52..011ec0b86 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -34,13 +34,13 @@ permissions: jobs: e2e: - name: Playwright Core E2E (shard ${{ matrix.shard }}/4) + name: Playwright Core E2E (shard ${{ matrix.shard }}/8) runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: fail-fast: false matrix: - shard: [1, 2, 3, 4] + shard: [1, 2, 3, 4, 5, 6, 7, 8] steps: - name: Checkout code @@ -83,8 +83,9 @@ jobs: PULSE_E2E_SKIP_DOCKER: "true" PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL: "true" PULSE_E2E_PERF: "1" + PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY: "true" PULSE_MULTI_TENANT_ENABLED: "true" - run: npm test -- --shard=${{ matrix.shard }}/4 + run: npm test -- --shard=${{ matrix.shard }}/8 - name: Collect container logs if: always() diff --git a/tests/integration/tests/68-platform-pages-shell.spec.ts b/tests/integration/tests/68-platform-pages-shell.spec.ts index 8ebde6473..a552ff35b 100644 --- a/tests/integration/tests/68-platform-pages-shell.spec.ts +++ b/tests/integration/tests/68-platform-pages-shell.spec.ts @@ -257,7 +257,9 @@ test.describe('Platform pages shell', () => { { path: '/docker/images', testId: 'docker-page' }, { path: '/docker/storage', testId: 'docker-page' }, { path: '/docker/networks', testId: 'docker-page' }, - { path: '/docker/swarm', testId: 'docker-page' }, + // The default mock inventory intentionally has no Swarm resources. + // Empty inventory tabs omit search because there is nothing actionable + // to filter; populated Swarm table toolbars have component coverage. { path: '/kubernetes/nodes', testId: 'kubernetes-page' }, { path: '/kubernetes/workloads', testId: 'kubernetes-page' }, { path: '/kubernetes/services', testId: 'kubernetes-page' }, diff --git a/tests/integration/tests/helpers.ts b/tests/integration/tests/helpers.ts index ff0826e45..4121344ea 100644 --- a/tests/integration/tests/helpers.ts +++ b/tests/integration/tests/helpers.ts @@ -1112,9 +1112,14 @@ async function storageStateHasLiveSession( baseURL: preferredBrowserBaseURL(), storageState: statePath, }); + const page = await context.newPage(); try { const res = await context.request.get("/api/state"); - return res.status() === 200; + if (res.status() !== 200) { + return false; + } + await waitForDefaultMockRuntimeReady(page); + return true; } catch { return false; } finally { @@ -1122,6 +1127,83 @@ async function storageStateHasLiveSession( } } +type E2ERuntimeStateResource = { + name?: string; + sources?: string[]; + type?: string; +}; + +type E2ERuntimeState = { + connectedInfrastructure?: Array<{ name?: string }>; + resources?: E2ERuntimeStateResource[]; +}; + +const requiresDefaultMockRuntimeReadiness = (): boolean => + ["1", "true", "yes", "on"].includes( + String(process.env.PULSE_E2E_REQUIRE_DEFAULT_MOCK_READY || "") + .trim() + .toLowerCase(), + ); + +const resourceHasSource = ( + resource: E2ERuntimeStateResource, + source: string, +): boolean => resource.sources?.includes(source) === true; + +async function waitForDefaultMockRuntimeReady(page: Page): Promise { + if (!requiresDefaultMockRuntimeReadiness()) { + return; + } + + await expect + .poll( + async () => { + const response = await page.request.get("/api/state").catch(() => null); + if (!response?.ok()) { + return false; + } + + const state = (await response.json()) as E2ERuntimeState; + const resources = Array.isArray(state.resources) ? state.resources : []; + const infrastructure = Array.isArray(state.connectedInfrastructure) + ? state.connectedInfrastructure + : []; + + return ( + resources.some( + (resource) => + resource.name === "nvme-primary" && + resource.type === "storage" && + resourceHasSource(resource, "vmware"), + ) && + resources.some( + (resource) => + resource.type === "k8s-cluster" && + resourceHasSource(resource, "kubernetes"), + ) && + resources.some( + (resource) => + resource.name === "tank" && + resourceHasSource(resource, "truenas"), + ) && + resources.some((resource) => resource.type === "docker-host") && + resources.some((resource) => resource.type === "pbs") && + resources.some((resource) => resource.type === "pmg") && + infrastructure.some( + (entry) => entry.name === "esxi-01.lab.local", + ) + ); + }, + { + message: + "default mock inventory should be complete before browser fixtures start", + timeout: 120_000, + intervals: [1_000, 2_000, 5_000], + }, + ) + .toBe(true); +} + export async function createAuthenticatedStorageState( browser: Browser, storageStatePath: string, @@ -1144,6 +1226,7 @@ export async function createAuthenticatedStorageState( const page = await context.newPage(); try { await ensureSessionAuthenticated(page); + await waitForDefaultMockRuntimeReady(page); await context.storageState({ path: storageStatePath }); } finally { await context.close();