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
5 changes: 5 additions & 0 deletions tests/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
test-results/
playwright-report/
blob-report/
.playwright/
27 changes: 27 additions & 0 deletions tests/e2e/helpers/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Shared authentication helper for Caldera UI tests.
*
* Caldera's default credentials are admin:admin. Override via env vars
* CALDERA_USER / CALDERA_PASS if the instance uses something else.
*/
const CALDERA_USER = process.env.CALDERA_USER || "admin";
const CALDERA_PASS = process.env.CALDERA_PASS || "admin";

/**
* Log into Caldera through the login page.
* After this resolves the page is authenticated and ready.
*/
async function login(page) {
await page.goto("/");

// If we are already past the login screen, nothing to do.
if (page.url().includes("/login") || (await page.locator('input[name="username"], input#username').count()) > 0) {
await page.locator('input[name="username"], input#username').first().fill(CALDERA_USER);
await page.locator('input[name="password"], input#password').first().fill(CALDERA_PASS);
await page.locator('button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign")').first().click();
// Wait for navigation away from login
await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 15_000 });
}
}

module.exports = { login, CALDERA_USER, CALDERA_PASS };
25 changes: 25 additions & 0 deletions tests/e2e/helpers/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Navigation helpers for reaching plugin tabs inside Caldera / Magma.
*/

/**
* Navigate to the Training plugin tab in the Magma Vue app.
* The training plugin registers under the "Training" nav item.
*/
async function navigateToTraining(page) {
// Magma renders a left-nav or top-nav with plugin names.
// Click the Training entry to load the plugin view.
const navItem = page.locator(
'a:has-text("Training"), .nav-item:has-text("Training"), [data-test="nav-training"], button:has-text("Training")'
).first();
await navItem.waitFor({ state: "visible", timeout: 15_000 });
await navItem.click();

// Wait for the training page root element to appear
await page.locator("#trainingPage, h2:has-text('Training')").first().waitFor({
state: "visible",
timeout: 15_000,
});
}

module.exports = { navigateToTraining };
78 changes: 78 additions & 0 deletions tests/e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "training-e2e-tests",
"version": "1.0.0",
"private": true,
"description": "Playwright E2E tests for the CALDERA Training plugin",
"scripts": {
"test": "npx playwright test",
"test:headed": "npx playwright test --headed",
"test:debug": "npx playwright test --debug",
"test:report": "npx playwright show-report"
},
"devDependencies": {
"@playwright/test": "^1.52.0"
}
}
31 changes: 31 additions & 0 deletions tests/e2e/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check
const { defineConfig, devices } = require("@playwright/test");

const CALDERA_URL = process.env.CALDERA_URL || "http://localhost:8888";

module.exports = defineConfig({
testDir: "./specs",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [["html", { open: "never" }], ["list"]],
timeout: 60_000,
expect: { timeout: 15_000 },

use: {
baseURL: CALDERA_URL,
trace: "on-first-retry",
screenshot: "only-on-failure",
actionTimeout: 10_000,
navigationTimeout: 30_000,
ignoreHTTPSErrors: true,
},

projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
133 changes: 133 additions & 0 deletions tests/e2e/specs/training-certificate-selection.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// @ts-check
const { test, expect } = require("@playwright/test");
const { login } = require("../helpers/auth");
const { navigateToTraining } = require("../helpers/navigation");

test.describe("Training plugin - certificate / badge selection", () => {
test.beforeEach(async ({ page }) => {
await login(page);
await navigateToTraining(page);
});

test("should list available certificates from the API", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();
// Wait for the certs API before checking options
await page.waitForResponse((resp) => resp.url().includes('/plugin/training/certs') && resp.status() === 200, { timeout: 15_000 });
// There should be at least 1 real option beyond the placeholder
const options = select.locator("option:not([disabled])");
const count = await options.count();
expect(count).toBeGreaterThanOrEqual(1);
});

test("should show Red Certificate option", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();
await page.waitForResponse((resp) => resp.url().includes('/plugin/training/certs') && resp.status() === 200, { timeout: 15_000 });
const redOption = select.locator('option:has-text("Red")');
const redCount = await redOption.count();
if (redCount === 0) {
test.skip(true, "No Red Certificate option available on this server");
return;
}
expect(redCount).toBeGreaterThanOrEqual(1);
});

test("should show Blue Certificate option", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();
await page.waitForResponse((resp) => resp.url().includes('/plugin/training/certs') && resp.status() === 200, { timeout: 15_000 });
const blueOption = select.locator('option:has-text("Blue")');
const blueCount = await blueOption.count();
if (blueCount === 0) {
test.skip(true, "No Blue Certificate option available on this server");
return;
}
expect(blueCount).toBeGreaterThanOrEqual(1);
});

test("selecting a certificate should load badges", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();

// Pick the first non-disabled option
const firstOption = select.locator("option:not([disabled])").first();
const optionValue = await firstOption.getAttribute("value");
if (!optionValue) return; // guard

await select.selectOption(optionValue);

// Badges should appear - they render as .badge-container-button elements
const badges = page.locator(".badge-container-button, .badge-text");
await expect(badges.first()).toBeVisible({ timeout: 15_000 });
});

test("clicking a badge should filter visible flags", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();

const firstOption = select.locator("option:not([disabled])").first();
const optionValue = await firstOption.getAttribute("value");
if (!optionValue) return;
await select.selectOption(optionValue);

// Wait for badges
const badges = page.locator(".badge-container-button");
await expect(badges.first()).toBeVisible({ timeout: 15_000 });

// Count initial flags
const flagsBefore = page.locator(".flag-card");
await expect(flagsBefore.first()).toBeVisible({ timeout: 10_000 });
const totalFlags = await flagsBefore.count();

// Click first badge to filter
await badges.first().click();

// After filtering, flag count should change (either same or fewer)
const flagsAfter = page.locator(".flag-card");
const filteredCount = await flagsAfter.count();
expect(filteredCount).toBeLessThanOrEqual(totalFlags);
expect(filteredCount).toBeGreaterThanOrEqual(1);
});

test("clicking a selected badge again should deselect it and show all flags", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();

const firstOption = select.locator("option:not([disabled])").first();
const optionValue = await firstOption.getAttribute("value");
if (!optionValue) return;
await select.selectOption(optionValue);

const badges = page.locator(".badge-container-button");
await expect(badges.first()).toBeVisible({ timeout: 15_000 });
const flagsBefore = page.locator(".flag-card");
await expect(flagsBefore.first()).toBeVisible({ timeout: 10_000 });
const totalFlags = await flagsBefore.count();

// Select then deselect
await badges.first().click();
await badges.first().click();

// Should show all flags again
const flagsAfter = page.locator(".flag-card");
const restoredCount = await flagsAfter.count();
expect(restoredCount).toBe(totalFlags);
});

test("selected badge should have the selected-badge CSS class", async ({ page }) => {
const select = page.locator("#select-certificate select").first();
await expect(select).toBeVisible();

const firstOption = select.locator("option:not([disabled])").first();
const optionValue = await firstOption.getAttribute("value");
if (!optionValue) return;
await select.selectOption(optionValue);

const badges = page.locator(".badge-container-button");
await expect(badges.first()).toBeVisible({ timeout: 15_000 });

await badges.first().click();
await expect(badges.first()).toHaveClass(/selected-badge/);
});
});
Loading
Loading