Skip to content

Add exhaustive Playwright E2E tests for Atomic plugin#51

Merged
deacon-mp merged 2 commits intomasterfrom
test/exhaustive-ui-e2e-tests
Mar 18, 2026
Merged

Add exhaustive Playwright E2E tests for Atomic plugin#51
deacon-mp merged 2 commits intomasterfrom
test/exhaustive-ui-e2e-tests

Conversation

@deacon-mp
Copy link
Copy Markdown
Contributor

Summary

  • Adds 22 Playwright E2E tests across 4 spec files for the Atomic plugin UI
  • Tests cover: page load, abilities display/count, ability ingestion verification, and error states
  • Tests run against a live Caldera instance via CALDERA_URL env var (default http://localhost:8888)
  • Includes shared auth and navigation helpers, Playwright config, and package.json

Test plan

  • Start a Caldera instance with the Atomic plugin enabled
  • Run cd tests/e2e && npm install && npx playwright install chromium && npx playwright test
  • Verify all 22 tests pass against the running instance
  • Test with CALDERA_URL env var pointing to a non-default port

22 tests across 4 spec files covering page load, abilities display
and count, ability ingestion verification, and error states. Tests
run against a live Caldera instance via CALDERA_URL env var
(default http://localhost:8888).
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Playwright-based E2E test suite targeting the Atomic plugin UI, intended to validate key UI flows and API-driven states against a running Caldera instance (via CALDERA_URL).

Changes:

  • Added 4 Playwright spec files covering Atomic page load, abilities display, ingestion-related API behavior, and error/edge states.
  • Introduced shared Playwright helpers for authentication and navigation.
  • Added Playwright configuration plus a standalone tests/e2e npm package (with lockfile and local .gitignore).

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/e2e/specs/atomic-page-load.spec.js Basic smoke checks for UI load and Atomic navigation/heading presence
tests/e2e/specs/atomic-abilities-display.spec.js Validates abilities count/label and “View Abilities” navigation
tests/e2e/specs/atomic-ingestion.spec.js Attempts to validate abilities/adversaries API calls and atomic filtering
tests/e2e/specs/atomic-error-states.spec.js Mocks failure/empty/slow/malformed API responses and checks UI resilience
tests/e2e/helpers/auth.js Shared login helper using env-overridable credentials
tests/e2e/helpers/navigation.js Shared helper to navigate to the Atomic tab
tests/e2e/playwright.config.js Playwright runner configuration (baseURL, timeouts, single-worker)
tests/e2e/package.json Defines Playwright dev dependency and test scripts
tests/e2e/package-lock.json Locks Playwright dependency tree for reproducible installs
tests/e2e/.gitignore Ignores Playwright artifacts and node_modules within tests/e2e
Files not reviewed (1)
  • tests/e2e/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to +114
await page.route("**/api/v2/abilities", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: "not valid json",
});
});
Comment on lines +31 to +37
await page.route("**/api/v2/abilities", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
Comment on lines +52 to +58
await page.route("**/api/v2/adversaries", (route) => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal Server Error" }),
});
});
Comment on lines +54 to +73
test("atomic abilities should have plugin field set to 'atomic'", async ({ page }) => {
let abilitiesData = null;
await page.route("**/api/v2/abilities", async (route) => {
const response = await route.fetch();
abilitiesData = await response.json();
await route.fulfill({ response });
});

await navigateToAtomic(page);
await page.waitForTimeout(5_000);

if (abilitiesData && Array.isArray(abilitiesData)) {
const atomicOnes = abilitiesData.filter((a) => a.plugin === "atomic");
// The count on the page should match the filtered count
const countText = await page.locator(".is-size-1, h1.is-size-1").first().textContent();
const displayedCount = parseInt(countText?.trim() || "0", 10);
if (!isNaN(displayedCount) && displayedCount > 0) {
expect(atomicOnes.length).toBe(displayedCount);
}
}
Comment on lines +77 to +86
const apiCalled = page.waitForResponse(
(resp) => resp.url().includes("/api/v2/adversaries") && resp.status() === 200,
{ timeout: 20_000 }
).catch(() => null);

await navigateToAtomic(page);
const response = await apiCalled;
if (response) {
expect(response.status()).toBe(200);
}
Comment on lines +86 to +93
await page.route("**/api/v2/abilities", async (route) => {
await new Promise((r) => setTimeout(r, 5_000));
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
Comment on lines +65 to +69
// Wait for count to potentially become a number (may stay --- if no abilities)
await page.waitForTimeout(5_000);
const text = await countText.textContent();
// Should be either a valid number or "---" (if no atomic abilities ingested)
expect(text?.trim()).toMatch(/^(\d+|---)$/);
Comment on lines +68 to +74
await page.route("**/api/v2/abilities", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
Comment on lines +16 to +28
const apiCalled = page.waitForResponse(
(resp) => resp.url().includes("/api/v2/abilities") && resp.status() === 200,
{ timeout: 20_000 }
).catch(() => null);

// Reload to trigger the API call fresh
await page.reload();
await navigateToAtomic(page);

const response = await apiCalled;
if (response) {
expect(response.status()).toBe(200);
}
Comment on lines +31 to +51
test("the abilities API response should contain ability objects", async ({ page }) => {
// Intercept abilities API
let abilitiesData = null;
await page.route("**/api/v2/abilities", async (route) => {
const response = await route.fetch();
abilitiesData = await response.json();
await route.fulfill({ response });
});

await navigateToAtomic(page);
await page.waitForTimeout(5_000);

// If we got abilities data, verify structure
if (abilitiesData && Array.isArray(abilitiesData)) {
// Each ability should have standard fields
if (abilitiesData.length > 0) {
const first = abilitiesData[0];
expect(first).toHaveProperty("ability_id");
expect(first).toHaveProperty("name");
}
}
atomic-error-states.spec.js:
- Add return before all route.fulfill() calls (6 instances) so the
  promise is properly returned to Playwright's route handler machinery

atomic-ingestion.spec.js:
- Remove .catch(() => null) from waitForResponse calls so timeouts
  propagate as test failures rather than silently passing
- Replace page.waitForTimeout(5000) with waitForResponse to reliably
  detect when the abilities API has been called
- Assert abilitiesData is an array and non-empty, and verify structure
  of the first ability object
- Assert displayedCount is a valid integer and matches the API result
  count rather than silently skipping when data is missing

atomic-abilities-display.spec.js:
- Replace waitForTimeout(5000) with condition-based wait using
  expect(countText).not.toHaveText('---') so the test waits for
  actual DOM change rather than an arbitrary fixed delay
@deacon-mp
Copy link
Copy Markdown
Contributor Author

@copilot review

Copy link
Copy Markdown

Copilot AI commented Mar 18, 2026

@deacon-mp I've opened a new pull request, #58, to work on those changes. Once the pull request is ready, I'll request review from you.

@deacon-mp deacon-mp merged commit 7138172 into master Mar 18, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants