Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7017352
test(e2e): add network error recovery & token expiration tests
MuhammadKhalilzadeh Mar 28, 2026
b6092f2
test(e2e): add Command Palette (Ctrl+K) tests
MuhammadKhalilzadeh Mar 28, 2026
0f30f57
test(e2e): add share links and model lifecycle detail tests
MuhammadKhalilzadeh Mar 28, 2026
1ad6df1
test(e2e): add change history / activity log tests
MuhammadKhalilzadeh Mar 28, 2026
caa0ad7
test(e2e): add rich text editor save/publish workflow tests
MuhammadKhalilzadeh Mar 28, 2026
6f927cf
test(e2e): add AI Gateway Playground chat tests
MuhammadKhalilzadeh Mar 28, 2026
d47a072
test(e2e): add report generation & export tests
MuhammadKhalilzadeh Mar 28, 2026
0251155
test(e2e): add dashboard widgets & views tests
MuhammadKhalilzadeh Mar 28, 2026
6a9f186
test(e2e): add post-market monitoring tests
MuhammadKhalilzadeh Mar 28, 2026
ad83931
test(e2e): add snackbar/toast notification tests
MuhammadKhalilzadeh Mar 28, 2026
78a1b0b
test(e2e): add pagination & table sorting tests
MuhammadKhalilzadeh Mar 28, 2026
473903d
test(e2e): add AI Detection scan workflow tests
MuhammadKhalilzadeh Mar 28, 2026
932f4e6
test(e2e): add Shadow AI rules & alerts tests
MuhammadKhalilzadeh Mar 28, 2026
5f5efbc
test(e2e): add framework navigation & compliance tests
MuhammadKhalilzadeh Mar 28, 2026
4ff93ca
test(e2e): add plugin marketplace tests
MuhammadKhalilzadeh Mar 28, 2026
b8d02c0
test(e2e): add onboarding wizard tests
MuhammadKhalilzadeh Mar 28, 2026
bd75bb4
test(e2e): add multi-step modal & dataset bulk upload tests
MuhammadKhalilzadeh Mar 28, 2026
790899f
fix(e2e): fix network resilience, AI gateway, and monitoring test fai…
MuhammadKhalilzadeh Mar 28, 2026
ca42e22
failed tests fixed
MuhammadKhalilzadeh Mar 30, 2026
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
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npx playwright test:*)"
]
}
}
58 changes: 58 additions & 0 deletions Clients/e2e/ai-detection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,62 @@ test.describe("AI Detection", () => {
.or(page.getByText(/no.*scan/i));
await expect(content.first()).toBeVisible({ timeout: 10_000 });
});

// --- Tier 5: Scan workflow ---

test("scan page shows URL input and scan button", async ({
authedPage: page,
}) => {
await page.goto("/ai-detection/scan");
await page.waitForTimeout(2000);

// Verify repository URL input
const urlInput = page
.locator("#repository-url")
.or(page.getByPlaceholder(/github/i))
.or(page.getByPlaceholder(/repo/i))
.or(page.getByRole("textbox"));

if (await urlInput.first().isVisible().catch(() => false)) {
await expect(urlInput.first()).toBeVisible();
}

// Verify scan button
const scanBtn = page
.getByRole("button", { name: /^scan$/i })
.or(page.getByRole("button", { name: /start scan/i }));

if (await scanBtn.first().isVisible().catch(() => false)) {
await expect(scanBtn.first()).toBeVisible();
}
});

test("history page shows past scans or empty state", async ({
authedPage: page,
}) => {
await page.goto("/ai-detection/history");
await page.waitForTimeout(2000);

const content = page
.getByRole("table")
.or(page.getByText(/no.*scan/i))
.or(page.getByText(/history/i))
.or(page.getByRole("heading"))
.or(page.getByText(/scan/i));

await expect(content.first()).toBeVisible({ timeout: 15_000 });
});

test("repositories page loads", async ({ authedPage: page }) => {
await page.goto("/ai-detection/repositories");
await page.waitForTimeout(2000);

const content = page
.getByText(/repositor/i)
.or(page.getByRole("table"))
.or(page.getByText(/no.*repositor/i))
.or(page.getByRole("heading"));

await expect(content.first()).toBeVisible({ timeout: 15_000 });
});
});
96 changes: 96 additions & 0 deletions Clients/e2e/ai-gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,100 @@ test.describe("AI Gateway", () => {
}
}
});

// --- Tier 5: Playground chat ---

test.describe("Playground", () => {
test("playground page renders composer or empty state", async ({
authedPage: page,
}) => {
await page.goto("/ai-gateway/playground");
await page.waitForTimeout(2000);

// Should show either a chat composer or an empty state
const content = page
.locator("[data-composer-input]")
.or(page.getByPlaceholder(/type a message/i))
.or(page.getByText(/no endpoint/i))
.or(page.getByText(/select an endpoint/i))
.or(page.getByText(/setup required/i))
.or(page.getByRole("combobox"));

await expect(content.first()).toBeVisible({ timeout: 15_000 });
});

test("endpoint selector shows available endpoints", async ({
authedPage: page,
}) => {
await page.goto("/ai-gateway/playground");
await page.waitForTimeout(2000);

// MUI Select renders a hidden input#endpoint + a visible div[role=combobox]
const endpointSelect = page
.locator('#endpoint ~ div[role="combobox"]')
.or(page.locator('#endpoint').locator('..').getByRole("combobox"))
.or(page.getByRole("combobox").first());

if (!(await endpointSelect.first().isVisible().catch(() => false))) {
test.skip();
return;
}

await endpointSelect.first().click();
await page.waitForTimeout(500);

// Check if options appear
const option = page.getByRole("option");
if (await option.first().isVisible().catch(() => false)) {
await expect(option.first()).toBeVisible();
}

await page.keyboard.press("Escape");
});

test("settings modal opens with temperature and token controls", async ({
authedPage: page,
}) => {
await page.goto("/ai-gateway/playground");
await page.waitForTimeout(2000);

// Find and click settings button
const settingsBtn = page
.getByRole("button", { name: /settings/i })
.or(page.locator('[aria-label*="settings" i]'));

if (!(await settingsBtn.first().isVisible().catch(() => false))) {
test.skip();
return;
}

await settingsBtn.first().click();
await page.waitForTimeout(500);

// Verify modal with temperature and max tokens
const tempLabel = page
.getByText(/temperature/i)
.or(page.getByText(/temp/i));
const tokensLabel = page
.getByText(/max tokens/i)
.or(page.getByText(/tokens/i));

if (await tempLabel.first().isVisible().catch(() => false)) {
await expect(tempLabel.first()).toBeVisible();
}
if (await tokensLabel.first().isVisible().catch(() => false)) {
await expect(tokensLabel.first()).toBeVisible();
}

// Close the modal
const closeBtn = page
.getByRole("button", { name: /close|cancel/i })
.or(page.getByRole("button", { name: /save/i }));
if (await closeBtn.first().isVisible().catch(() => false)) {
await closeBtn.first().click();
} else {
await page.keyboard.press("Escape");
}
});
});
});
192 changes: 192 additions & 0 deletions Clients/e2e/command-palette.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { test as base, expect, type Page } from "@playwright/test";

/**
* Custom fixture for command palette tests.
* Uses 'domcontentloaded' wait strategy instead of 'load' to avoid
* timing out while the dashboard fetches many API resources.
*/
const test = base.extend<{ authedPage: Page }>({
authedPage: async ({ page }, use) => {
await page.goto("/vendors", { waitUntil: "domcontentloaded" });
await expect(page).not.toHaveURL(/\/login/, { timeout: 15_000 });
await use(page);
},
});

/**
* Helper to open the command palette by dispatching a synthetic keyboard event.
* We bypass page.keyboard.press("Control+k") because Chromium may intercept
* Ctrl+K as a browser shortcut before it reaches the page.
*/
async function openCommandPalette(page: Page) {
await page.evaluate(() => {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "k",
ctrlKey: true,
bubbles: true,
cancelable: true,
})
);
});
await page.waitForTimeout(500);
}

/** Locator for the command palette search input */
function getSearchInput(page: Page) {
return page
.locator("[cmdk-input]")
.or(page.locator(".command-input"))
.or(page.getByRole("combobox"));
}

test.describe("Command Palette (Ctrl+K)", () => {
test("Ctrl+K opens command palette", async ({ authedPage: page }) => {
// Dismiss any welcome dialogs first
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
await welcomeSkip.click();
await page.waitForTimeout(1000);
}

// Open command palette via synthetic keyboard event
await openCommandPalette(page);

// Verify search input is present (proves the palette opened)
const searchInput = getSearchInput(page);
if (
!(await searchInput
.first()
.isVisible({ timeout: 5_000 })
.catch(() => false))
) {
test.skip();
return;
}
await expect(searchInput.first()).toBeVisible();

// Verify the dialog container is present
const dialog = page.locator(".command-dialog");
await expect(dialog).toBeVisible({ timeout: 3_000 });

await page.keyboard.press("Escape");
});

test("typing filters navigation commands", async ({ authedPage: page }) => {
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
await welcomeSkip.click();
await page.waitForTimeout(1000);
}

await openCommandPalette(page);

const searchInput = getSearchInput(page);
if (
!(await searchInput
.first()
.isVisible({ timeout: 5_000 })
.catch(() => false))
) {
test.skip();
return;
}

// Type a page name to filter
await searchInput.first().fill("vendor");
await page.waitForTimeout(500);

// Should show matching command items
const matchingItem = page
.locator("[cmdk-item]")
.or(page.locator(".command-item"))
.or(page.getByRole("option"))
.or(page.getByText(/vendor/i));

if (await matchingItem.first().isVisible().catch(() => false)) {
await expect(matchingItem.first()).toBeVisible();
}

await page.keyboard.press("Escape");
});

test("selecting a command navigates to page", async ({
authedPage: page,
}) => {
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
await welcomeSkip.click();
await page.waitForTimeout(1000);
}

await openCommandPalette(page);

const searchInput = getSearchInput(page);
if (
!(await searchInput
.first()
.isVisible({ timeout: 5_000 })
.catch(() => false))
) {
test.skip();
return;
}

// Search for tasks page
await searchInput.first().fill("tasks");
await page.waitForTimeout(500);

// Click on the matching item or press Enter
const matchingItem = page
.locator("[cmdk-item]")
.or(page.locator(".command-item"))
.or(page.getByRole("option"));

if (await matchingItem.first().isVisible().catch(() => false)) {
await matchingItem.first().click();
await page.waitForTimeout(1000);

// Verify navigation occurred
const navigated =
page.url().includes("/tasks") || page.url().includes("/search");
if (navigated) {
expect(page.url()).toContain("/");
}
} else {
// Try pressing Enter as fallback
await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
}
});

test("Escape closes command palette", async ({ authedPage: page }) => {
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
await welcomeSkip.click();
await page.waitForTimeout(1000);
}

// Open the command palette
await openCommandPalette(page);

// Verify it opened by checking the search input
const searchInput = getSearchInput(page);
if (
!(await searchInput
.first()
.isVisible({ timeout: 5_000 })
.catch(() => false))
) {
test.skip();
return;
}

// Press Escape to close
await page.keyboard.press("Escape");
await page.waitForTimeout(500);

// Verify the command palette dialog is no longer visible
const dialog = page.locator(".command-dialog");
await expect(dialog).not.toBeVisible({ timeout: 5_000 });
});
});
Loading
Loading