Skip to content

Commit 3f8c62a

Browse files
committed
test(e2e): add discovery spec — reports nav automation limits
Not a regression gate. Runs five journeys (event-scripts, api-db, roles, admins, mcp) and reports what breaks. First run surfaces one cross-cutting finding more than any per-journey bug: the admin sidebar is resistant to standard Playwright selectors. Concrete observations from the first run (see console output): - clicking any sidebar nav-item button (even with force:true) lands at /home instead of the target route. Suggests (a) the click event isn't reaching the Angular router handler, (b) navigation happens but a guard / welcome flow redirects, or (c) click falls through to a nearby element (the "Admins" click landed at /ai, which sits close by in the DOM). - no clickable [routerLink] or href on the nav-item elements; Angular binds via (click) on <mat-list-item> wrappers. - actionability without force: true always times out. Fixing this right likely needs data-testid attributes on nav items or a dedicated NavPage object that understands the accordion state. Parking for now — ships the spec as a runnable record of the automation gap.
1 parent be00ec4 commit 3f8c62a

1 file changed

Lines changed: 285 additions & 0 deletions

File tree

e2e/_findings.spec.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Discovery run. Not a regression gate — this spec is designed to FAIL
3+
* loudly on anything that isn't working so we get a concrete bug list.
4+
* Do not add to jest.config.ci or the nightly Playwright workflow.
5+
*
6+
* Strategy: simulate a new admin working through the most common journeys
7+
* on a vanilla dev-docker instance. Each step records what it saw; the
8+
* final console report lists every deviation from expected.
9+
*/
10+
import { test, expect, Page } from '@playwright/test';
11+
import { loginAsAdmin, waitForAppReady } from './fixtures/admin-login';
12+
13+
type Finding = {
14+
journey: string;
15+
step: string;
16+
detail: string;
17+
};
18+
const findings: Finding[] = [];
19+
function report(journey: string, step: string, detail: string) {
20+
findings.push({ journey, step, detail });
21+
console.log(` [${journey}] ${step}: ${detail}`);
22+
}
23+
24+
async function clickNavByText(
25+
page: Page,
26+
label: string,
27+
journey: string
28+
): Promise<boolean> {
29+
// Sidebar entries are <div mat-list-item> wrappers with a <button
30+
// .nav-item> and a <span .nav-item> inside. The router navigation is
31+
// bound to the mat-list-item, not the inner button — clicking only the
32+
// button appears to trigger but doesn't actually route. Click the
33+
// mat-list-item containing the target text.
34+
const el = page
35+
.locator(`[mat-list-item]:has(button.nav-item:has-text("${label}"))`)
36+
.first();
37+
const count = await el.count();
38+
if (count === 0) {
39+
report(journey, 'nav-select', `no mat-list-item matched "${label}"`);
40+
return false;
41+
}
42+
try {
43+
await el.scrollIntoViewIfNeeded({ timeout: 2_000 });
44+
} catch (e: any) {
45+
report(journey, 'nav-scroll', e.message.split('\n')[0]);
46+
}
47+
try {
48+
await el.click({ timeout: 5_000, force: true });
49+
return true;
50+
} catch (e: any) {
51+
report(journey, 'nav-click', e.message.split('\n')[0]);
52+
return false;
53+
}
54+
}
55+
56+
async function setupPage(page: Page) {
57+
await page.setViewportSize({ width: 1600, height: 900 });
58+
await loginAsAdmin(page);
59+
await waitForAppReady(page);
60+
// Wait for the sidebar nav to populate. Home button is always present;
61+
// if it never appears, the login flow didn't resolve.
62+
await page
63+
.locator('button.nav-item:has-text("Home")')
64+
.first()
65+
.waitFor({ timeout: 10_000 })
66+
.catch(() => {});
67+
}
68+
69+
test.describe.configure({ timeout: 240_000 });
70+
71+
test('journey: event-scripts create', async ({ page }) => {
72+
const J = 'event-scripts';
73+
const nav4xx: number[] = [];
74+
page.on('response', r => {
75+
if (r.url().includes('/api/') && r.status() >= 400) nav4xx.push(r.status());
76+
});
77+
page.on('pageerror', e => report(J, 'jsError', e.message));
78+
await setupPage(page);
79+
80+
// 1. Navigate
81+
const clicked = await clickNavByText(page, 'Event Scripts', J);
82+
if (!clicked) {
83+
report(J, 'nav', 'Event Scripts nav entry not clickable from fresh login');
84+
return;
85+
}
86+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
87+
const url = page.url();
88+
if (!/event-scripts/.test(url)) {
89+
report(J, 'nav', `expected /event-scripts, landed at ${url}`);
90+
return;
91+
}
92+
93+
// 2. Click +
94+
const addBtn = page.locator('button.save-btn').first();
95+
if (!(await addBtn.isVisible().catch(() => false))) {
96+
report(J, '+ button', '.save-btn not visible on /event-scripts list');
97+
return;
98+
}
99+
await addBtn.click().catch(e => report(J, '+ click', e.message));
100+
101+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
102+
103+
// 3. Create form — Service dropdown
104+
const svcSel = page.locator('mat-select').first();
105+
if (!(await svcSel.isVisible().catch(() => false))) {
106+
report(J, 'form', 'no mat-select visible on create form');
107+
return;
108+
}
109+
await svcSel.click();
110+
const svcOptions = await page.locator('mat-option').allTextContents();
111+
if (svcOptions.length === 0) {
112+
report(J, 'service dropdown', 'zero options');
113+
return;
114+
}
115+
report(J, 'service dropdown', `${svcOptions.length} options: ${svcOptions.slice(0, 5).join(', ')}…`);
116+
await page.locator('mat-option').filter({ hasText: /^db$/ }).first().click().catch(() => {});
117+
await page.waitForTimeout(500);
118+
119+
// 4. Script Type
120+
const typeSel = page.locator('mat-select').nth(1);
121+
await typeSel.click().catch(() => {});
122+
const typeOptions = await page.locator('mat-option').allTextContents();
123+
if (typeOptions.length === 0) {
124+
report(J, 'script type dropdown', 'zero options after picking service=db');
125+
return;
126+
}
127+
report(J, 'script type dropdown', `${typeOptions.length} options, first: ${typeOptions[0]}`);
128+
await page.locator('mat-option').first().click();
129+
await page.waitForTimeout(300);
130+
131+
// 5. Script Method
132+
const methodSel = page.locator('mat-select').nth(2);
133+
await methodSel.click().catch(() => {});
134+
const methodOptions = await page.locator('mat-option').allTextContents();
135+
if (methodOptions.length === 0) {
136+
report(J, 'script method dropdown', 'zero options after picking type');
137+
return;
138+
}
139+
report(J, 'script method dropdown', `${methodOptions.length} options, first: ${methodOptions[0]}`);
140+
await page.locator('mat-option').first().click();
141+
await page.waitForTimeout(300);
142+
143+
// 6. Save
144+
const saveBtn = page.locator('button[type="submit"]').first();
145+
if (!(await saveBtn.isVisible().catch(() => false))) {
146+
report(J, 'save button', 'not visible');
147+
return;
148+
}
149+
const [saveResp] = await Promise.all([
150+
page.waitForResponse(r => r.url().includes('/api/v2/system/event_script') && r.request().method() === 'POST', { timeout: 10_000 }).catch(() => null),
151+
saveBtn.click().catch(() => {}),
152+
]);
153+
if (!saveResp) {
154+
report(J, 'save', 'no POST /system/event_script fired — form submit did nothing');
155+
} else if (!saveResp.ok()) {
156+
report(J, 'save', `HTTP ${saveResp.status()} ${saveResp.statusText()}: ${(await saveResp.text()).slice(0, 200)}`);
157+
} else {
158+
report(J, 'save', `OK ${saveResp.status()}`);
159+
}
160+
161+
if (nav4xx.length) report(J, '4xx/5xx', `statuses: ${nav4xx.join(',')}`);
162+
});
163+
164+
test('journey: api-connections > database', async ({ page }) => {
165+
const J = 'api-db';
166+
page.on('pageerror', e => report(J, 'jsError', e.message));
167+
await setupPage(page);
168+
169+
if (!(await clickNavByText(page, 'Database', J))) {
170+
return;
171+
}
172+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
173+
report(J, 'nav', `landed at ${page.url()}`);
174+
175+
// Look for + button
176+
if (!(await page.locator('button.save-btn').first().isVisible().catch(() => false))) {
177+
report(J, '+ button', 'no .save-btn on Database list view');
178+
return;
179+
}
180+
await page.locator('button.save-btn').first().click();
181+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
182+
report(J, 'create form', `URL after +: ${page.url()}`);
183+
184+
// Is there a service type picker?
185+
const typeSel = page.locator('mat-select').first();
186+
if (!(await typeSel.isVisible().catch(() => false))) {
187+
report(J, 'type picker', 'no mat-select on create form');
188+
return;
189+
}
190+
await typeSel.click();
191+
const types = await page.locator('mat-option').count();
192+
report(J, 'type picker', `${types} service-type options`);
193+
});
194+
195+
test('journey: roles list', async ({ page }) => {
196+
const J = 'roles';
197+
page.on('pageerror', e => report(J, 'jsError', e.message));
198+
await setupPage(page);
199+
200+
const ok = await clickNavByText(page, 'Role Based Access', J);
201+
if (!ok) {
202+
report(J, 'nav', 'Role Based Access nav entry not clickable');
203+
return;
204+
}
205+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
206+
report(J, 'nav', `landed at ${page.url()}`);
207+
208+
// List view
209+
const rows = await page.locator('mat-row, tr.mat-mdc-row').count();
210+
report(J, 'list', `${rows} role rows rendered`);
211+
212+
if (!(await page.locator('button.save-btn').first().isVisible().catch(() => false))) {
213+
report(J, '+ button', 'no .save-btn on roles list');
214+
return;
215+
}
216+
await page.locator('button.save-btn').first().click();
217+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
218+
report(J, 'create form', `URL: ${page.url()}`);
219+
220+
const inputs = await page.locator('input[type="text"], mat-select').count();
221+
report(J, 'create form', `${inputs} inputs/selects on form`);
222+
});
223+
224+
test('journey: admin users list', async ({ page }) => {
225+
const J = 'admins';
226+
page.on('pageerror', e => report(J, 'jsError', e.message));
227+
await setupPage(page);
228+
229+
if (!(await clickNavByText(page, 'Admins', J))) {
230+
return;
231+
}
232+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
233+
report(J, 'nav', `landed at ${page.url()}`);
234+
235+
const rows = await page.locator('mat-row, tr.mat-mdc-row').count();
236+
report(J, 'list', `${rows} admin rows rendered`);
237+
238+
if (!(await page.locator('button.save-btn').first().isVisible().catch(() => false))) {
239+
report(J, '+ button', 'no .save-btn on admins list');
240+
return;
241+
}
242+
await page.locator('button.save-btn').first().click();
243+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
244+
report(J, 'create form', `URL: ${page.url()}`);
245+
});
246+
247+
test('journey: mcp create service', async ({ page }) => {
248+
const J = 'mcp';
249+
page.on('pageerror', e => report(J, 'jsError', e.message));
250+
await setupPage(page);
251+
252+
if (!(await clickNavByText(page, 'Utility', J))) {
253+
return;
254+
}
255+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
256+
report(J, 'nav', `landed at ${page.url()}`);
257+
258+
// Look for sub-nav MCP link
259+
const mcpLink = page.locator('a:has-text("MCP"), button:has-text("MCP")').first();
260+
if (!(await mcpLink.isVisible().catch(() => false))) {
261+
report(J, 'mcp link', 'no MCP sub-nav under Utility');
262+
return;
263+
}
264+
await mcpLink.click().catch(() => {});
265+
await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
266+
report(J, 'mcp page', `URL: ${page.url()}`);
267+
});
268+
269+
test.afterAll(() => {
270+
console.log('\n=== DISCOVERY FINDINGS ===\n');
271+
if (findings.length === 0) {
272+
console.log('(no issues recorded)');
273+
} else {
274+
const byJ = new Map<string, Finding[]>();
275+
for (const f of findings) {
276+
if (!byJ.has(f.journey)) byJ.set(f.journey, []);
277+
byJ.get(f.journey)!.push(f);
278+
}
279+
for (const [j, list] of byJ) {
280+
console.log(`\n== ${j} ==`);
281+
for (const f of list) console.log(` • ${f.step.padEnd(22)} ${f.detail}`);
282+
}
283+
}
284+
console.log('\n=== END FINDINGS ===\n');
285+
});

0 commit comments

Comments
 (0)