Skip to content

Commit 47ddf04

Browse files
Merge pull request #3629 from verifywise-ai/mo-240-mar-28-more-e2e-tests
Covering more advanced E2e tests
2 parents b420196 + ca42e22 commit 47ddf04

18 files changed

+1836
-0
lines changed

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npx playwright test:*)"
5+
]
6+
}
7+
}

Clients/e2e/ai-detection.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,62 @@ test.describe("AI Detection", () => {
5252
.or(page.getByText(/no.*scan/i));
5353
await expect(content.first()).toBeVisible({ timeout: 10_000 });
5454
});
55+
56+
// --- Tier 5: Scan workflow ---
57+
58+
test("scan page shows URL input and scan button", async ({
59+
authedPage: page,
60+
}) => {
61+
await page.goto("/ai-detection/scan");
62+
await page.waitForTimeout(2000);
63+
64+
// Verify repository URL input
65+
const urlInput = page
66+
.locator("#repository-url")
67+
.or(page.getByPlaceholder(/github/i))
68+
.or(page.getByPlaceholder(/repo/i))
69+
.or(page.getByRole("textbox"));
70+
71+
if (await urlInput.first().isVisible().catch(() => false)) {
72+
await expect(urlInput.first()).toBeVisible();
73+
}
74+
75+
// Verify scan button
76+
const scanBtn = page
77+
.getByRole("button", { name: /^scan$/i })
78+
.or(page.getByRole("button", { name: /start scan/i }));
79+
80+
if (await scanBtn.first().isVisible().catch(() => false)) {
81+
await expect(scanBtn.first()).toBeVisible();
82+
}
83+
});
84+
85+
test("history page shows past scans or empty state", async ({
86+
authedPage: page,
87+
}) => {
88+
await page.goto("/ai-detection/history");
89+
await page.waitForTimeout(2000);
90+
91+
const content = page
92+
.getByRole("table")
93+
.or(page.getByText(/no.*scan/i))
94+
.or(page.getByText(/history/i))
95+
.or(page.getByRole("heading"))
96+
.or(page.getByText(/scan/i));
97+
98+
await expect(content.first()).toBeVisible({ timeout: 15_000 });
99+
});
100+
101+
test("repositories page loads", async ({ authedPage: page }) => {
102+
await page.goto("/ai-detection/repositories");
103+
await page.waitForTimeout(2000);
104+
105+
const content = page
106+
.getByText(/repositor/i)
107+
.or(page.getByRole("table"))
108+
.or(page.getByText(/no.*repositor/i))
109+
.or(page.getByRole("heading"));
110+
111+
await expect(content.first()).toBeVisible({ timeout: 15_000 });
112+
});
55113
});

Clients/e2e/ai-gateway.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,100 @@ test.describe("AI Gateway", () => {
304304
}
305305
}
306306
});
307+
308+
// --- Tier 5: Playground chat ---
309+
310+
test.describe("Playground", () => {
311+
test("playground page renders composer or empty state", async ({
312+
authedPage: page,
313+
}) => {
314+
await page.goto("/ai-gateway/playground");
315+
await page.waitForTimeout(2000);
316+
317+
// Should show either a chat composer or an empty state
318+
const content = page
319+
.locator("[data-composer-input]")
320+
.or(page.getByPlaceholder(/type a message/i))
321+
.or(page.getByText(/no endpoint/i))
322+
.or(page.getByText(/select an endpoint/i))
323+
.or(page.getByText(/setup required/i))
324+
.or(page.getByRole("combobox"));
325+
326+
await expect(content.first()).toBeVisible({ timeout: 15_000 });
327+
});
328+
329+
test("endpoint selector shows available endpoints", async ({
330+
authedPage: page,
331+
}) => {
332+
await page.goto("/ai-gateway/playground");
333+
await page.waitForTimeout(2000);
334+
335+
// MUI Select renders a hidden input#endpoint + a visible div[role=combobox]
336+
const endpointSelect = page
337+
.locator('#endpoint ~ div[role="combobox"]')
338+
.or(page.locator('#endpoint').locator('..').getByRole("combobox"))
339+
.or(page.getByRole("combobox").first());
340+
341+
if (!(await endpointSelect.first().isVisible().catch(() => false))) {
342+
test.skip();
343+
return;
344+
}
345+
346+
await endpointSelect.first().click();
347+
await page.waitForTimeout(500);
348+
349+
// Check if options appear
350+
const option = page.getByRole("option");
351+
if (await option.first().isVisible().catch(() => false)) {
352+
await expect(option.first()).toBeVisible();
353+
}
354+
355+
await page.keyboard.press("Escape");
356+
});
357+
358+
test("settings modal opens with temperature and token controls", async ({
359+
authedPage: page,
360+
}) => {
361+
await page.goto("/ai-gateway/playground");
362+
await page.waitForTimeout(2000);
363+
364+
// Find and click settings button
365+
const settingsBtn = page
366+
.getByRole("button", { name: /settings/i })
367+
.or(page.locator('[aria-label*="settings" i]'));
368+
369+
if (!(await settingsBtn.first().isVisible().catch(() => false))) {
370+
test.skip();
371+
return;
372+
}
373+
374+
await settingsBtn.first().click();
375+
await page.waitForTimeout(500);
376+
377+
// Verify modal with temperature and max tokens
378+
const tempLabel = page
379+
.getByText(/temperature/i)
380+
.or(page.getByText(/temp/i));
381+
const tokensLabel = page
382+
.getByText(/max tokens/i)
383+
.or(page.getByText(/tokens/i));
384+
385+
if (await tempLabel.first().isVisible().catch(() => false)) {
386+
await expect(tempLabel.first()).toBeVisible();
387+
}
388+
if (await tokensLabel.first().isVisible().catch(() => false)) {
389+
await expect(tokensLabel.first()).toBeVisible();
390+
}
391+
392+
// Close the modal
393+
const closeBtn = page
394+
.getByRole("button", { name: /close|cancel/i })
395+
.or(page.getByRole("button", { name: /save/i }));
396+
if (await closeBtn.first().isVisible().catch(() => false)) {
397+
await closeBtn.first().click();
398+
} else {
399+
await page.keyboard.press("Escape");
400+
}
401+
});
402+
});
307403
});
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { test as base, expect, type Page } from "@playwright/test";
2+
3+
/**
4+
* Custom fixture for command palette tests.
5+
* Uses 'domcontentloaded' wait strategy instead of 'load' to avoid
6+
* timing out while the dashboard fetches many API resources.
7+
*/
8+
const test = base.extend<{ authedPage: Page }>({
9+
authedPage: async ({ page }, use) => {
10+
await page.goto("/vendors", { waitUntil: "domcontentloaded" });
11+
await expect(page).not.toHaveURL(/\/login/, { timeout: 15_000 });
12+
await use(page);
13+
},
14+
});
15+
16+
/**
17+
* Helper to open the command palette by dispatching a synthetic keyboard event.
18+
* We bypass page.keyboard.press("Control+k") because Chromium may intercept
19+
* Ctrl+K as a browser shortcut before it reaches the page.
20+
*/
21+
async function openCommandPalette(page: Page) {
22+
await page.evaluate(() => {
23+
document.dispatchEvent(
24+
new KeyboardEvent("keydown", {
25+
key: "k",
26+
ctrlKey: true,
27+
bubbles: true,
28+
cancelable: true,
29+
})
30+
);
31+
});
32+
await page.waitForTimeout(500);
33+
}
34+
35+
/** Locator for the command palette search input */
36+
function getSearchInput(page: Page) {
37+
return page
38+
.locator("[cmdk-input]")
39+
.or(page.locator(".command-input"))
40+
.or(page.getByRole("combobox"));
41+
}
42+
43+
test.describe("Command Palette (Ctrl+K)", () => {
44+
test("Ctrl+K opens command palette", async ({ authedPage: page }) => {
45+
// Dismiss any welcome dialogs first
46+
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
47+
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
48+
await welcomeSkip.click();
49+
await page.waitForTimeout(1000);
50+
}
51+
52+
// Open command palette via synthetic keyboard event
53+
await openCommandPalette(page);
54+
55+
// Verify search input is present (proves the palette opened)
56+
const searchInput = getSearchInput(page);
57+
if (
58+
!(await searchInput
59+
.first()
60+
.isVisible({ timeout: 5_000 })
61+
.catch(() => false))
62+
) {
63+
test.skip();
64+
return;
65+
}
66+
await expect(searchInput.first()).toBeVisible();
67+
68+
// Verify the dialog container is present
69+
const dialog = page.locator(".command-dialog");
70+
await expect(dialog).toBeVisible({ timeout: 3_000 });
71+
72+
await page.keyboard.press("Escape");
73+
});
74+
75+
test("typing filters navigation commands", async ({ authedPage: page }) => {
76+
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
77+
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
78+
await welcomeSkip.click();
79+
await page.waitForTimeout(1000);
80+
}
81+
82+
await openCommandPalette(page);
83+
84+
const searchInput = getSearchInput(page);
85+
if (
86+
!(await searchInput
87+
.first()
88+
.isVisible({ timeout: 5_000 })
89+
.catch(() => false))
90+
) {
91+
test.skip();
92+
return;
93+
}
94+
95+
// Type a page name to filter
96+
await searchInput.first().fill("vendor");
97+
await page.waitForTimeout(500);
98+
99+
// Should show matching command items
100+
const matchingItem = page
101+
.locator("[cmdk-item]")
102+
.or(page.locator(".command-item"))
103+
.or(page.getByRole("option"))
104+
.or(page.getByText(/vendor/i));
105+
106+
if (await matchingItem.first().isVisible().catch(() => false)) {
107+
await expect(matchingItem.first()).toBeVisible();
108+
}
109+
110+
await page.keyboard.press("Escape");
111+
});
112+
113+
test("selecting a command navigates to page", async ({
114+
authedPage: page,
115+
}) => {
116+
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
117+
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
118+
await welcomeSkip.click();
119+
await page.waitForTimeout(1000);
120+
}
121+
122+
await openCommandPalette(page);
123+
124+
const searchInput = getSearchInput(page);
125+
if (
126+
!(await searchInput
127+
.first()
128+
.isVisible({ timeout: 5_000 })
129+
.catch(() => false))
130+
) {
131+
test.skip();
132+
return;
133+
}
134+
135+
// Search for tasks page
136+
await searchInput.first().fill("tasks");
137+
await page.waitForTimeout(500);
138+
139+
// Click on the matching item or press Enter
140+
const matchingItem = page
141+
.locator("[cmdk-item]")
142+
.or(page.locator(".command-item"))
143+
.or(page.getByRole("option"));
144+
145+
if (await matchingItem.first().isVisible().catch(() => false)) {
146+
await matchingItem.first().click();
147+
await page.waitForTimeout(1000);
148+
149+
// Verify navigation occurred
150+
const navigated =
151+
page.url().includes("/tasks") || page.url().includes("/search");
152+
if (navigated) {
153+
expect(page.url()).toContain("/");
154+
}
155+
} else {
156+
// Try pressing Enter as fallback
157+
await page.keyboard.press("Enter");
158+
await page.waitForTimeout(1000);
159+
}
160+
});
161+
162+
test("Escape closes command palette", async ({ authedPage: page }) => {
163+
const welcomeSkip = page.getByRole("button", { name: /skip for now/i });
164+
if (await welcomeSkip.isVisible({ timeout: 3_000 }).catch(() => false)) {
165+
await welcomeSkip.click();
166+
await page.waitForTimeout(1000);
167+
}
168+
169+
// Open the command palette
170+
await openCommandPalette(page);
171+
172+
// Verify it opened by checking the search input
173+
const searchInput = getSearchInput(page);
174+
if (
175+
!(await searchInput
176+
.first()
177+
.isVisible({ timeout: 5_000 })
178+
.catch(() => false))
179+
) {
180+
test.skip();
181+
return;
182+
}
183+
184+
// Press Escape to close
185+
await page.keyboard.press("Escape");
186+
await page.waitForTimeout(500);
187+
188+
// Verify the command palette dialog is no longer visible
189+
const dialog = page.locator(".command-dialog");
190+
await expect(dialog).not.toBeVisible({ timeout: 5_000 });
191+
});
192+
});

0 commit comments

Comments
 (0)