blob: 0498df67baadc8f22fdd4c34218f0c4127da97ed [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect, request, Page } from '@playwright/test';
import * as path from 'path';
import * as fs from 'fs';
const API = 'http://localhost:8080';
const UI = 'http://localhost:4000';
const GRAFANA = 'http://localhost:3002';
const SCREENSHOT_DIR = path.join(__dirname, 'screenshots');
// Use existing connection with valid credentials
const EXISTING_CONNECTION_ID = 5;
const state: {
connectionId: number;
scopeId: string;
blueprintId: number;
pipelineId: number;
} = { connectionId: EXISTING_CONNECTION_ID, scopeId: '', blueprintId: 0, pipelineId: 0 };
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
async function grafanaLogin(page: Page) {
await page.goto(`${GRAFANA}/grafana/login`);
await page.waitForLoadState('networkidle');
if (page.url().includes('/login')) {
await page.locator('input[name="user"]').fill('admin');
await page.locator('input[name="password"]').fill('admin');
await page.locator('button[type="submit"]').click();
await page.waitForTimeout(2000);
// Handle "change password" prompt if shown
const skipBtn = page.locator('a:has-text("Skip")');
if (await skipBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
await skipBtn.click();
}
await page.waitForTimeout(1000);
}
}
async function openGrafanaDashboard(page: Page, uid: string, screenshotPath: string) {
await grafanaLogin(page);
await page.goto(`${GRAFANA}/grafana/d/${uid}?orgId=1&from=now-90d&to=now`);
// Wait for first panel data to load
try {
await page.waitForResponse(
(resp) => resp.url().includes('/api/ds/query') && resp.status() === 200,
{ timeout: 30000 }
);
} catch { /* some dashboards may not fire queries immediately */ }
// Wait for rendering to settle
await page.waitForTimeout(5000);
// Take viewport screenshot (top section)
await page.screenshot({ path: screenshotPath.replace('.png', '-top.png') });
// Scroll down and take more sections
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
let section = 1;
for (let y = 900; y < scrollHeight; y += 900) {
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
await page.waitForTimeout(3000);
section++;
await page.screenshot({ path: screenshotPath.replace('.png', `-section${section}.png`) });
}
// Also take full page screenshot
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(2000);
await page.screenshot({ path: screenshotPath, fullPage: true });
}
test.describe.serial('Q-Dev Plugin Full Flow', () => {
test('Step 1: Verify Existing Connection via API', async () => {
const api = await request.newContext({ baseURL: API });
const resp = await api.get(`/plugins/q_dev/connections/${state.connectionId}`);
expect(resp.ok()).toBeTruthy();
const conn = await resp.json();
console.log(`Using connection: id=${conn.id}, name=${conn.name}, bucket=${conn.bucket}`);
const testResp = await api.post(`/plugins/q_dev/connections/${state.connectionId}/test`);
const testBody = await testResp.json();
console.log('Test connection:', testBody.success ? 'OK' : testBody.message);
expect(testResp.ok()).toBeTruthy();
});
test('Step 2: View Config-UI Home', async ({ page }) => {
await page.goto(UI);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '01-config-ui-home.png'), fullPage: true });
console.log('Screenshot: Config-UI home');
});
test('Step 3: Create Scope (S3 Slice) via API', async () => {
const api = await request.newContext({ baseURL: API });
const resp = await api.put(`/plugins/q_dev/connections/${state.connectionId}/scopes`, {
data: {
data: [
{
accountId: '034362076319',
basePath: '',
year: 2026,
month: 3,
},
],
},
});
const body = await resp.json();
console.log('Scope created:', resp.status());
expect(resp.ok()).toBeTruthy();
state.scopeId = body[0]?.id;
expect(state.scopeId).toBeTruthy();
console.log(`Scope id: ${state.scopeId}`);
});
test('Step 4: Create Blueprint via API', async () => {
const api = await request.newContext({ baseURL: API });
const resp = await api.post('/blueprints', {
data: {
name: `e2e-blueprint-${Date.now()}`,
mode: 'NORMAL',
enable: true,
cronConfig: '0 0 * * *',
isManual: true,
connections: [
{
pluginName: 'q_dev',
connectionId: state.connectionId,
scopes: [{ scopeId: state.scopeId }],
},
],
},
});
const body = await resp.json();
expect(resp.ok()).toBeTruthy();
state.blueprintId = body.id;
console.log(`Blueprint created: id=${state.blueprintId}`);
});
test('Step 5: Trigger Pipeline via API', async () => {
const api = await request.newContext({ baseURL: API });
const resp = await api.post(`/blueprints/${state.blueprintId}/trigger`, { data: {} });
const body = await resp.json();
expect(resp.ok()).toBeTruthy();
state.pipelineId = body.id;
console.log(`Pipeline triggered: id=${state.pipelineId}`);
});
test('Step 6: Wait for Pipeline to Complete', async () => {
const api = await request.newContext({ baseURL: API });
const maxWait = 120000;
const start = Date.now();
let status = '';
while (Date.now() - start < maxWait) {
const resp = await api.get(`/pipelines/${state.pipelineId}`);
const pipeline = await resp.json();
status = pipeline.status;
console.log(`Pipeline status: ${status} (${Math.round((Date.now() - start) / 1000)}s)`);
if (['TASK_COMPLETED', 'TASK_FAILED', 'TASK_PARTIAL'].includes(status)) break;
await new Promise((r) => setTimeout(r, 3000));
}
// Print task details
const tasksResp = await api.get(`/pipelines/${state.pipelineId}/tasks`);
if (tasksResp.ok()) {
const { tasks } = await tasksResp.json();
for (const t of tasks || []) {
console.log(` Task ${t.id}: ${t.status}${t.failedSubTask ? ` (failed: ${t.failedSubTask})` : ''}`);
if (t.message) console.log(` Error: ${t.message.substring(0, 300)}`);
}
}
expect(status).toBe('TASK_COMPLETED');
});
test('Step 7: Verify Data via MySQL', async () => {
const api = await request.newContext({ baseURL: API });
// Use pipeline tasks to confirm data was processed
const tasksResp = await api.get(`/pipelines/${state.pipelineId}/tasks`);
const { tasks } = await tasksResp.json();
expect(tasks[0].status).toBe('TASK_COMPLETED');
console.log(`Pipeline completed in ${tasks[0].spentSeconds}s`);
});
test('Step 8: Grafana - Kiro Usage Dashboard (new format)', async ({ page }) => {
await openGrafanaDashboard(page, 'qdev_user_report', path.join(SCREENSHOT_DIR, '02-dashboard-user-report.png'));
console.log('Screenshot: Kiro Usage Dashboard');
});
test('Step 9: Grafana - Kiro Legacy Feature Metrics', async ({ page }) => {
await openGrafanaDashboard(page, 'qdev_feature_metrics', path.join(SCREENSHOT_DIR, '03-dashboard-feature-metrics.png'));
console.log('Screenshot: Kiro Legacy Feature Metrics');
});
test('Step 10: Grafana - Kiro AI Activity Insights (logging)', async ({ page }) => {
await openGrafanaDashboard(page, 'qdev_logging', path.join(SCREENSHOT_DIR, '04-dashboard-logging.png'));
console.log('Screenshot: Kiro AI Activity Insights');
});
test('Step 11: Grafana - Kiro Executive Dashboard', async ({ page }) => {
await openGrafanaDashboard(page, 'qdev_executive', path.join(SCREENSHOT_DIR, '05-dashboard-executive.png'));
console.log('Screenshot: Kiro Executive Dashboard');
});
test('Step 12: View Pipeline in Config-UI', async ({ page }) => {
// Navigate to the API proxy route for pipelines
await page.goto(`${UI}/api/pipelines?pageSize=5`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '06-config-ui-pipelines.png'), fullPage: true });
console.log('Screenshot: Pipelines API response');
});
test('Step 13: Cleanup', async () => {
const api = await request.newContext({ baseURL: API });
if (state.blueprintId) {
await api.delete(`/blueprints/${state.blueprintId}`);
console.log(`Deleted blueprint ${state.blueprintId}`);
}
console.log('Cleanup complete');
});
});