855 lines
39 KiB
JavaScript
855 lines
39 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
const path = require('path');
|
|
|
|
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'https://tilbudsgiveren.alw.dk';
|
|
const ARTIFACTS_DIR = path.join(__dirname, 'artifacts');
|
|
|
|
const LOGIN_USERNAME = 'toemrer';
|
|
const LOGIN_PASSWORD = 'tilbud2024';
|
|
|
|
/**
|
|
* Shared login helper with retry.
|
|
*/
|
|
async function login(page, maxRetries = 3) {
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
console.log(`[LOGIN] Attempt ${attempt}/${maxRetries}...`);
|
|
await page.goto(BASE_URL + '/', { waitUntil: 'networkidle', timeout: 30000 });
|
|
await page.waitForTimeout(1000);
|
|
|
|
const loginHeading = page.getByRole('heading', { name: /Log ind/i });
|
|
const isLogin = await loginHeading.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!isLogin) {
|
|
const passwordField = page.locator('input[type="password"]').first();
|
|
const hasPasswordField = await passwordField.isVisible({ timeout: 3000 }).catch(() => false);
|
|
if (!hasPasswordField) {
|
|
console.log('[LOGIN] Already authenticated');
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.log('[LOGIN] Filling credentials...');
|
|
const usernameField = page.locator('input[type="text"]').first();
|
|
await usernameField.click();
|
|
await usernameField.fill('');
|
|
await page.waitForTimeout(200);
|
|
await usernameField.fill(LOGIN_USERNAME);
|
|
|
|
const passwordField = page.locator('input[type="password"]').first();
|
|
await passwordField.click();
|
|
await passwordField.fill('');
|
|
await page.waitForTimeout(200);
|
|
await passwordField.fill(LOGIN_PASSWORD);
|
|
await page.waitForTimeout(500);
|
|
|
|
const loginBtn = page.getByRole('button', { name: /Log ind/i });
|
|
await loginBtn.click();
|
|
await page.waitForTimeout(3000);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
|
|
const stillOnLogin = await page.getByRole('heading', { name: /Log ind/i }).isVisible().catch(() => false);
|
|
if (!stillOnLogin) {
|
|
console.log('[LOGIN] Login successful');
|
|
return;
|
|
}
|
|
|
|
const bodyText = await page.locator('body').textContent();
|
|
const errorMatch = bodyText.match(/(fejl|forkert|ugyldig|error)[^.]{0,100}/i);
|
|
console.log(`[LOGIN] Attempt ${attempt} failed: "${errorMatch ? errorMatch[0] : 'unknown'}"`);
|
|
if (attempt < maxRetries) await page.waitForTimeout(2000);
|
|
}
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'login-failed.png'), fullPage: true });
|
|
throw new Error(`Login failed after ${maxRetries} attempts`);
|
|
}
|
|
|
|
/**
|
|
* Setup console error and network failure tracking
|
|
*/
|
|
function setupMonitoring(page) {
|
|
const consoleErrors = [];
|
|
const networkFailures = [];
|
|
const allApiCalls = [];
|
|
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
consoleErrors.push({ text: msg.text(), location: msg.location() });
|
|
}
|
|
});
|
|
|
|
page.on('response', response => {
|
|
const url = response.url();
|
|
const status = response.status();
|
|
if (url.includes('/api/') || url.includes('/graphql')) {
|
|
allApiCalls.push({ url, status, statusText: response.statusText() });
|
|
}
|
|
if (status >= 400) {
|
|
networkFailures.push({
|
|
url, status, statusText: response.statusText(),
|
|
method: response.request().method()
|
|
});
|
|
}
|
|
});
|
|
|
|
page.on('requestfailed', request => {
|
|
networkFailures.push({
|
|
url: request.url(), status: 0, statusText: 'REQUEST_FAILED',
|
|
method: request.method(), failure: request.failure()?.errorText || 'unknown'
|
|
});
|
|
});
|
|
|
|
return { consoleErrors, networkFailures, allApiCalls };
|
|
}
|
|
|
|
function reportFindings(testName, { consoleErrors, networkFailures, allApiCalls }) {
|
|
console.log(`\n========== ${testName} REPORT ==========`);
|
|
console.log(`\n--- API Calls (${allApiCalls.length}) ---`);
|
|
allApiCalls.forEach(call => {
|
|
const shortUrl = call.url.length > 120 ? call.url.substring(0, 120) + '...' : call.url;
|
|
const ok = call.status >= 200 && call.status < 400;
|
|
console.log(` [${ok ? 'OK' : 'FAIL'}] ${call.status} ${shortUrl}`);
|
|
});
|
|
console.log(`\n--- Network Failures (${networkFailures.length}) ---`);
|
|
networkFailures.forEach(fail => {
|
|
const shortUrl = fail.url.length > 120 ? fail.url.substring(0, 120) + '...' : fail.url;
|
|
console.log(` [${fail.method}] ${fail.status} ${fail.statusText} ${shortUrl}`);
|
|
if (fail.failure) console.log(` Failure: ${fail.failure}`);
|
|
});
|
|
console.log(`\n--- Console Errors (${consoleErrors.length}) ---`);
|
|
consoleErrors.forEach(err => {
|
|
const text = err.text.length > 300 ? err.text.substring(0, 300) + '...' : err.text;
|
|
console.log(` ${text}`);
|
|
});
|
|
console.log(`\n========== END ${testName} REPORT ==========\n`);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// TEST 1: Create New Smart Package
|
|
// ============================================================
|
|
test.describe('TEST 1: Create New Smart Package', () => {
|
|
test.setTimeout(120000);
|
|
|
|
test('Navigate to wizard, fill form, attempt save, capture errors', async ({ page }) => {
|
|
const monitoring = setupMonitoring(page);
|
|
|
|
// Step 1: Login
|
|
console.log('\n[TEST1] Step 1: Logging in...');
|
|
await login(page);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-01-after-login.png'), fullPage: true });
|
|
|
|
// Step 2: Click "Smart Pakker & Opgaver" in top nav
|
|
console.log('[TEST1] Step 2: Clicking "Smart Pakker & Opgaver" in top navigation...');
|
|
const smartPakkerNavBtn = page.getByRole('button', { name: /Smart Pakker/i });
|
|
await expect(smartPakkerNavBtn).toBeVisible({ timeout: 10000 });
|
|
await smartPakkerNavBtn.click();
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-02-smart-pakker-dashboard.png'), fullPage: true });
|
|
|
|
// Step 3: Click "Ny Smart Pakke" in the left sidebar
|
|
console.log('[TEST1] Step 3: Clicking "Ny Smart Pakke" in sidebar...');
|
|
const nySPBtn = page.locator('text=Ny Smart Pakke').first();
|
|
await expect(nySPBtn).toBeVisible({ timeout: 5000 });
|
|
await nySPBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-03-create-wizard.png'), fullPage: true });
|
|
|
|
// Verify wizard is showing - should see "Opret Smart Pakke" heading and stepper
|
|
const wizardHeading = page.locator('text=/Opret Smart Pakke/i');
|
|
const hasWizard = await wizardHeading.isVisible().catch(() => false);
|
|
console.log(`[TEST1] Wizard "Opret Smart Pakke" visible: ${hasWizard}`);
|
|
|
|
// Step 4: Fill in "Pakkenavn" field
|
|
console.log('[TEST1] Step 4: Filling in form fields...');
|
|
|
|
// The Pakkenavn field - find it by label
|
|
const pakkenavnLabel = page.locator('text=Pakkenavn').first();
|
|
const pakkenavnVisible = await pakkenavnLabel.isVisible().catch(() => false);
|
|
console.log(`[TEST1] Pakkenavn label visible: ${pakkenavnVisible}`);
|
|
|
|
// Get all visible input fields and list them
|
|
const allInputs = page.locator('input:visible');
|
|
const inputCount = await allInputs.count();
|
|
console.log(`[TEST1] Visible input fields: ${inputCount}`);
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const inp = allInputs.nth(i);
|
|
const name = await inp.getAttribute('name').catch(() => null);
|
|
const placeholder = await inp.getAttribute('placeholder').catch(() => null);
|
|
const type = await inp.getAttribute('type').catch(() => null);
|
|
const value = await inp.inputValue().catch(() => null);
|
|
console.log(`[TEST1] Input[${i}]: name="${name}" type="${type}" placeholder="${placeholder}" value="${value}"`);
|
|
}
|
|
|
|
// Fill Pakkenavn - try finding the input near the label
|
|
// From screenshot: the first text input after "Pakkenavn *" label
|
|
const nameInput = page.locator('input').first();
|
|
const nameInputType = await nameInput.getAttribute('type').catch(() => '');
|
|
console.log(`[TEST1] First input type: "${nameInputType}"`);
|
|
|
|
// Use keyboard to type instead of fill (to work around React controlled input issues)
|
|
await nameInput.click();
|
|
await nameInput.fill('');
|
|
await page.waitForTimeout(200);
|
|
|
|
// Type character by character to avoid [object Object] bug
|
|
await nameInput.pressSequentially('Test Pakke E2E', { delay: 50 });
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify what got entered
|
|
const nameValue = await nameInput.inputValue().catch(() => '');
|
|
console.log(`[TEST1] Pakkenavn value after typing: "${nameValue}"`);
|
|
|
|
// BUG CHECK: Does the field show [object Object]?
|
|
if (nameValue === '[object Object]' || nameValue.includes('[object Object]')) {
|
|
console.log('[TEST1] BUG DETECTED: Pakkenavn field shows "[object Object]" - this is a React state management bug');
|
|
console.log('[TEST1] The input.fill() method causes an event object to be stored instead of the string value');
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-04-pakkenavn-filled.png'), fullPage: true });
|
|
|
|
// Fill Kategori - it's a "Vaelg kategori" dropdown
|
|
console.log('[TEST1] Filling Kategori...');
|
|
const kategoriSelect = page.locator('text=Vaelg kategori').first();
|
|
const kategoriAlt = page.locator('select, [role="combobox"], [class*="Select"]').first();
|
|
|
|
if (await kategoriSelect.isVisible().catch(() => false)) {
|
|
await kategoriSelect.click();
|
|
await page.waitForTimeout(500);
|
|
// Try to select an option from dropdown
|
|
const menuItem = page.locator('[role="option"], .MuiMenuItem-root, li').filter({ hasText: /Tag|Renovering|Reparation/i }).first();
|
|
if (await menuItem.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const optionText = await menuItem.textContent().catch(() => '');
|
|
await menuItem.click();
|
|
console.log(`[TEST1] Selected kategori: "${optionText}"`);
|
|
} else {
|
|
console.log('[TEST1] No kategori options visible after clicking');
|
|
// List what is visible
|
|
const options = page.locator('[role="option"], .MuiMenuItem-root');
|
|
const optCount = await options.count().catch(() => 0);
|
|
console.log(`[TEST1] Found ${optCount} option elements`);
|
|
}
|
|
} else if (await kategoriAlt.isVisible().catch(() => false)) {
|
|
await kategoriAlt.click();
|
|
await page.waitForTimeout(500);
|
|
} else {
|
|
console.log('[TEST1] Kategori selector not found');
|
|
}
|
|
|
|
// Fill Beskrivelse (textarea)
|
|
console.log('[TEST1] Filling Beskrivelse...');
|
|
const beskrivelse = page.locator('textarea').first();
|
|
if (await beskrivelse.isVisible().catch(() => false)) {
|
|
await beskrivelse.click();
|
|
await beskrivelse.pressSequentially('Test beskrivelse for E2E test', { delay: 30 });
|
|
const descValue = await beskrivelse.inputValue().catch(() => '');
|
|
console.log(`[TEST1] Beskrivelse value: "${descValue.substring(0, 80)}"`);
|
|
} else {
|
|
console.log('[TEST1] Beskrivelse textarea not found');
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-05-form-filled.png'), fullPage: true });
|
|
|
|
// Step 5: Click "NAESTE" to advance the wizard
|
|
console.log('[TEST1] Step 5: Clicking NAESTE to advance wizard...');
|
|
const naesteBtn = page.locator('button').filter({ hasText: /N.*STE|N.*ste|Next/i }).first();
|
|
if (await naesteBtn.isVisible().catch(() => false)) {
|
|
const naesteBtnText = await naesteBtn.textContent().catch(() => '');
|
|
console.log(`[TEST1] Found next button: "${naesteBtnText.trim()}"`);
|
|
await naesteBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Check for validation errors
|
|
const bodyAfterNext = await page.locator('body').textContent();
|
|
const validationErrors = bodyAfterNext.match(/(påkrævet|required|udfyld|fill|fejl|error|mangler|missing)[^.]{0,150}/gi);
|
|
if (validationErrors) {
|
|
validationErrors.forEach(err => console.log(`[TEST1] VALIDATION ERROR: "${err}"`));
|
|
}
|
|
} else {
|
|
console.log('[TEST1] NAESTE button not found');
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-06-after-naeste.png'), fullPage: true });
|
|
|
|
// Check if we advanced to step 2 or got validation errors
|
|
const bodyTextStep2 = await page.locator('body').textContent();
|
|
const onStep2 = bodyTextStep2.includes('Tilf') && bodyTextStep2.includes('Materialer');
|
|
const stillStep1 = bodyTextStep2.includes('Pakkenavn');
|
|
console.log(`[TEST1] Advanced to Step 2 (Tilfoj Materialer): ${onStep2}`);
|
|
console.log(`[TEST1] Still on Step 1: ${stillStep1}`);
|
|
|
|
// If on step 2, try to skip to step 4 (Gennemse & Gem)
|
|
if (onStep2) {
|
|
console.log('[TEST1] On step 2, advancing to final step...');
|
|
// Click NAESTE two more times to get to step 4
|
|
for (let step = 0; step < 2; step++) {
|
|
const nextBtn = page.locator('button').filter({ hasText: /N.*STE|N.*ste|Next/i }).first();
|
|
if (await nextBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(1500);
|
|
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
|
|
}
|
|
}
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-07-step4-review.png'), fullPage: true });
|
|
|
|
// Now look for "Gem" (Save) button on step 4
|
|
const gemBtn = page.locator('button').filter({ hasText: /Gem|Save|Opret Pakke|Opret Smart/i }).first();
|
|
if (await gemBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const gemText = await gemBtn.textContent().catch(() => '');
|
|
console.log(`[TEST1] Found save button: "${gemText.trim()}"`);
|
|
|
|
// Set up response listener before clicking
|
|
const responsePromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/smart-packages') && resp.request().method() === 'POST',
|
|
{ timeout: 15000 }
|
|
).catch(() => null);
|
|
|
|
await gemBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
|
|
const saveResponse = await responsePromise;
|
|
if (saveResponse) {
|
|
console.log(`[TEST1] POST /api/smart-packages response: ${saveResponse.status()} ${saveResponse.statusText()}`);
|
|
try {
|
|
const respBody = await saveResponse.text();
|
|
console.log(`[TEST1] Response body: ${respBody.substring(0, 500)}`);
|
|
} catch (e) {
|
|
console.log(`[TEST1] Could not read response: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log('[TEST1] No POST to /api/smart-packages detected');
|
|
}
|
|
} else {
|
|
console.log('[TEST1] No Gem/Save button found on final step');
|
|
// List visible buttons
|
|
const btns = page.locator('button:visible');
|
|
const btnCount = await btns.count();
|
|
for (let i = 0; i < btnCount; i++) {
|
|
const text = await btns.nth(i).textContent().catch(() => '');
|
|
if (text.trim()) console.log(`[TEST1] Button[${i}]: "${text.trim().substring(0, 80)}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 6: Capture final error/success messages
|
|
console.log('[TEST1] Step 6: Capturing final state...');
|
|
|
|
// Check for any alerts or snackbars
|
|
const alerts = page.locator('[role="alert"], .MuiAlert-root, [class*="Snackbar"], [class*="snackbar"]');
|
|
const alertCount = await alerts.count().catch(() => 0);
|
|
for (let i = 0; i < alertCount; i++) {
|
|
const visible = await alerts.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const alertText = await alerts.nth(i).textContent().catch(() => '');
|
|
console.log(`[TEST1] ALERT/SNACKBAR: "${alertText.trim().substring(0, 300)}"`);
|
|
}
|
|
}
|
|
|
|
// Check for error classes
|
|
const errorEls = page.locator('[class*="error"], [class*="Error"]');
|
|
const errorCount = await errorEls.count().catch(() => 0);
|
|
for (let i = 0; i < Math.min(errorCount, 10); i++) {
|
|
const visible = await errorEls.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const errText = await errorEls.nth(i).textContent().catch(() => '');
|
|
if (errText.trim() && errText.length < 200) {
|
|
console.log(`[TEST1] ERROR ELEMENT: "${errText.trim()}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
reportFindings('TEST 1: Create Smart Package', monitoring);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test1-08-final-state.png'), fullPage: true });
|
|
});
|
|
});
|
|
|
|
|
|
// ============================================================
|
|
// TEST 2: Planning Module
|
|
// ============================================================
|
|
test.describe('TEST 2: Planning Module', () => {
|
|
test.setTimeout(120000);
|
|
|
|
test('Navigate to planning, wait for data, diagnose loading issue', async ({ page }) => {
|
|
const monitoring = setupMonitoring(page);
|
|
|
|
// Step 1: Login
|
|
console.log('\n[TEST2] Step 1: Logging in...');
|
|
await login(page);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test2-01-after-login.png'), fullPage: true });
|
|
|
|
// Step 2: Click "Planlaegning" in top nav
|
|
console.log('[TEST2] Step 2: Clicking "Planlaegning" in top navigation...');
|
|
const planningBtn = page.getByRole('button', { name: /Planl.*gning/i });
|
|
await expect(planningBtn).toBeVisible({ timeout: 10000 });
|
|
await planningBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test2-02-planning-initial.png'), fullPage: true });
|
|
|
|
// Step 3: Wait for data with detailed tracking
|
|
console.log('[TEST2] Step 3: Monitoring API responses and waiting for data...');
|
|
|
|
// Track specific planning API calls
|
|
const planningApiCalls = [];
|
|
page.on('response', response => {
|
|
if (response.url().includes('/api/planning/')) {
|
|
planningApiCalls.push({
|
|
url: response.url(),
|
|
status: response.status(),
|
|
statusText: response.statusText()
|
|
});
|
|
}
|
|
});
|
|
|
|
// Wait up to 30 seconds for the loading spinner to disappear
|
|
const loadingText = page.locator('text=/Indl.*ser.*planl.*gningsdata/i');
|
|
const spinnerGone = await loadingText.waitFor({ state: 'hidden', timeout: 30000 }).then(() => true).catch(() => false);
|
|
console.log(`[TEST2] Loading spinner disappeared after wait: ${spinnerGone}`);
|
|
|
|
if (!spinnerGone) {
|
|
console.log('[TEST2] BUG: Loading spinner still showing after 30 seconds');
|
|
|
|
// Check if the loading text is still visible
|
|
const stillLoading = await loadingText.isVisible().catch(() => false);
|
|
console.log(`[TEST2] "Indlaeser planlaegningsdata..." still visible: ${stillLoading}`);
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test2-03-after-30s-wait.png'), fullPage: true });
|
|
|
|
// Step 4: Inspect API responses in detail
|
|
console.log('[TEST2] Step 4: Inspecting planning API responses...');
|
|
console.log(`[TEST2] Planning API calls detected: ${planningApiCalls.length}`);
|
|
planningApiCalls.forEach(call => {
|
|
console.log(`[TEST2] ${call.status} ${call.url}`);
|
|
});
|
|
|
|
// Also check from monitoring
|
|
const calendarCalls = monitoring.allApiCalls.filter(c => c.url.includes('/api/planning/'));
|
|
console.log(`[TEST2] Total planning API calls from monitoring: ${calendarCalls.length}`);
|
|
calendarCalls.forEach(call => {
|
|
console.log(`[TEST2] ${call.status} ${call.url}`);
|
|
});
|
|
|
|
// Try to fetch the APIs directly to see their response content
|
|
console.log('[TEST2] Step 5: Direct API response inspection...');
|
|
|
|
// Check calendar API response
|
|
try {
|
|
const calendarResponse = await page.evaluate(async () => {
|
|
const token = localStorage.getItem('accessToken');
|
|
const today = new Date();
|
|
const startDate = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()).toISOString().split('T')[0];
|
|
const endDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate()).toISOString().split('T')[0];
|
|
const resp = await fetch(`/api/planning/calendar?startDate=${startDate}&endDate=${endDate}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const text = await resp.text();
|
|
return { status: resp.status, body: text.substring(0, 1000) };
|
|
});
|
|
console.log(`[TEST2] Calendar API direct call: ${calendarResponse.status}`);
|
|
console.log(`[TEST2] Calendar API response body: ${calendarResponse.body}`);
|
|
} catch (e) {
|
|
console.log(`[TEST2] Calendar API direct call failed: ${e.message}`);
|
|
}
|
|
|
|
// Check employees API response
|
|
try {
|
|
const employeesResponse = await page.evaluate(async () => {
|
|
const token = localStorage.getItem('accessToken');
|
|
const resp = await fetch('/api/planning/employees', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const text = await resp.text();
|
|
return { status: resp.status, body: text.substring(0, 1000) };
|
|
});
|
|
console.log(`[TEST2] Employees API direct call: ${employeesResponse.status}`);
|
|
console.log(`[TEST2] Employees API response body: ${employeesResponse.body}`);
|
|
} catch (e) {
|
|
console.log(`[TEST2] Employees API direct call failed: ${e.message}`);
|
|
}
|
|
|
|
// Check orders/cases API if it exists
|
|
try {
|
|
const ordersResponse = await page.evaluate(async () => {
|
|
const token = localStorage.getItem('accessToken');
|
|
const resp = await fetch('/api/planning/active-orders', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const text = await resp.text();
|
|
return { status: resp.status, body: text.substring(0, 1000) };
|
|
});
|
|
console.log(`[TEST2] Orders API direct call: ${ordersResponse.status}`);
|
|
console.log(`[TEST2] Orders API response body: ${ordersResponse.body}`);
|
|
} catch (e) {
|
|
console.log(`[TEST2] Orders API direct call failed: ${e.message}`);
|
|
}
|
|
|
|
// Step 6: Check page state after waiting
|
|
console.log('[TEST2] Step 6: Final page state analysis...');
|
|
|
|
const bodyText = await page.locator('body').textContent();
|
|
|
|
// Check for specific content types
|
|
const checks = [
|
|
{ name: 'Orders/Cases', pattern: /ordre|order|sag|case/i },
|
|
{ name: 'Employees', pattern: /medarbejder|employee|ansat|mont.*r|t.*mrer/i },
|
|
{ name: 'Calendar', pattern: /kalender|calendar|uge|week|mandag|tirsdag|onsdag/i },
|
|
{ name: 'Loading text', pattern: /indl.*ser|loading/i },
|
|
{ name: 'Error messages', pattern: /fejl|error|kunne ikke/i },
|
|
{ name: 'Empty state', pattern: /ingen.*data|no.*data|tom|empty/i },
|
|
{ name: 'Ordrestyring', pattern: /ordrestyring/i },
|
|
];
|
|
|
|
checks.forEach(check => {
|
|
const match = bodyText.match(check.pattern);
|
|
console.log(`[TEST2] ${check.name}: ${match ? `YES - "${match[0]}"` : 'NO'}`);
|
|
});
|
|
|
|
// Check for loading spinners / progress indicators
|
|
const spinners = page.locator('[role="progressbar"], [class*="CircularProgress"], [class*="spinner"], [class*="Spinner"]');
|
|
const spinnerCount = await spinners.count().catch(() => 0);
|
|
let visibleSpinners = 0;
|
|
for (let i = 0; i < spinnerCount; i++) {
|
|
if (await spinners.nth(i).isVisible().catch(() => false)) visibleSpinners++;
|
|
}
|
|
console.log(`[TEST2] Visible loading spinners: ${visibleSpinners}`);
|
|
|
|
// Log first portion of page text for diagnosis
|
|
console.log(`[TEST2] Page body text (first 800 chars): ${bodyText.substring(0, 800)}`);
|
|
|
|
// Check for errors in UI
|
|
const errorEls = page.locator('[role="alert"], .MuiAlert-root, [class*="error-message"]');
|
|
const errCount = await errorEls.count().catch(() => 0);
|
|
for (let i = 0; i < errCount; i++) {
|
|
const visible = await errorEls.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const errText = await errorEls.nth(i).textContent().catch(() => '');
|
|
console.log(`[TEST2] UI ERROR: "${errText.trim().substring(0, 300)}"`);
|
|
}
|
|
}
|
|
|
|
reportFindings('TEST 2: Planning Module', monitoring);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test2-04-final-state.png'), fullPage: true });
|
|
});
|
|
});
|
|
|
|
|
|
// ============================================================
|
|
// TEST 3: AI Quote Text Generation
|
|
// ============================================================
|
|
test.describe('TEST 3: AI Quote Text Generation', () => {
|
|
test.setTimeout(180000);
|
|
|
|
test('Open existing project, navigate through wizard to Final Review, trigger AI generation', async ({ page }) => {
|
|
const monitoring = setupMonitoring(page);
|
|
|
|
// Step 1: Login
|
|
console.log('\n[TEST3] Step 1: Logging in...');
|
|
await login(page);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-01-after-login.png'), fullPage: true });
|
|
|
|
// Step 2: Make sure we're on "Projekt Flow" tab
|
|
console.log('[TEST3] Step 2: Ensuring we are on Projekt Flow...');
|
|
const projektFlowBtn = page.getByRole('button', { name: /Projekt Flow/i });
|
|
if (await projektFlowBtn.isVisible().catch(() => false)) {
|
|
await projektFlowBtn.click();
|
|
await page.waitForTimeout(1500);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
// Step 3: Click "Eksisterende" tab to see existing projects
|
|
console.log('[TEST3] Step 3: Clicking "Eksisterende" tab...');
|
|
const eksisterendeBtn = page.locator('text=/Eksisterende/i').first();
|
|
await expect(eksisterendeBtn).toBeVisible({ timeout: 10000 });
|
|
await eksisterendeBtn.click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-02-existing-projects.png'), fullPage: true });
|
|
|
|
// Step 4: Click "Fortsaet projekt" on the FIRST project card
|
|
console.log('[TEST3] Step 4: Clicking "Fortsaet projekt"...');
|
|
const fortsaetBtns = page.locator('button').filter({ hasText: /Forts.*t projekt/i });
|
|
const fortsaetCount = await fortsaetBtns.count().catch(() => 0);
|
|
console.log(`[TEST3] Found ${fortsaetCount} "Fortsaet projekt" buttons`);
|
|
|
|
if (fortsaetCount > 0) {
|
|
await fortsaetBtns.first().click();
|
|
await page.waitForTimeout(3000);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-03-inside-project.png'), fullPage: true });
|
|
|
|
// Step 5: Navigate through the wizard steps to reach Final Review
|
|
// The wizard has 4 steps: Projekt, Geometri, Smart Pakke, Final Review
|
|
// Step 4 (Final Review) requires packageData and geometry. We need to go through steps.
|
|
console.log('[TEST3] Step 5: Navigating through wizard steps...');
|
|
|
|
// First, check what step we're on and list all step nav items
|
|
const stepNavItems = page.locator('.step, [class*="steps-nav"] > div, [class*="step-nav"] > div');
|
|
const stepNavCount = await stepNavItems.count().catch(() => 0);
|
|
console.log(`[TEST3] Step navigation items: ${stepNavCount}`);
|
|
for (let i = 0; i < stepNavCount; i++) {
|
|
const text = await stepNavItems.nth(i).textContent().catch(() => '');
|
|
const cls = await stepNavItems.nth(i).getAttribute('class').catch(() => '');
|
|
if (text.trim()) {
|
|
const isActive = cls.includes('active');
|
|
const isDisabled = cls.includes('disabled');
|
|
const isAvailable = cls.includes('available');
|
|
console.log(`[TEST3] Step nav[${i}]: "${text.trim().substring(0, 40)}" active=${isActive} disabled=${isDisabled} available=${isAvailable}`);
|
|
}
|
|
}
|
|
|
|
// Try clicking the "Final Review" step directly in the nav
|
|
const finalReviewStep = page.locator('.step, [class*="step"]').filter({ hasText: /Final Review/i });
|
|
const frStepCount = await finalReviewStep.count().catch(() => 0);
|
|
console.log(`[TEST3] Final Review step elements: ${frStepCount}`);
|
|
|
|
let reachedFinalReview = false;
|
|
|
|
if (frStepCount > 0) {
|
|
const frClass = await finalReviewStep.first().getAttribute('class').catch(() => '');
|
|
const isAvailable = frClass.includes('available') && !frClass.includes('disabled');
|
|
console.log(`[TEST3] Final Review step class: "${frClass}" available: ${isAvailable}`);
|
|
|
|
if (isAvailable) {
|
|
await finalReviewStep.first().click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Check if we landed on Final Review
|
|
const bodyNow = await page.locator('body').textContent();
|
|
reachedFinalReview = bodyNow.includes('Tilbudstekst') || bodyNow.includes('Final Review - Tilbud');
|
|
console.log(`[TEST3] Reached Final Review via click: ${reachedFinalReview}`);
|
|
} else {
|
|
console.log('[TEST3] Final Review step is disabled/not available - need to go through prior steps');
|
|
}
|
|
}
|
|
|
|
// If not reachable directly, navigate through Geometry -> Smart Pakke -> Final Review
|
|
if (!reachedFinalReview) {
|
|
// Try clicking "Fortsaet til Smart Pakke" or "Smart Pakke" step
|
|
console.log('[TEST3] Trying to navigate through steps sequentially...');
|
|
|
|
// Check if "Fortsaet til Smart Pakke" button exists (from Geometry step)
|
|
const fortsaetSP = page.locator('button').filter({ hasText: /Forts.*t til Smart Pakke|Smart Pakke/i });
|
|
if (await fortsaetSP.first().isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const spBtnText = await fortsaetSP.first().textContent().catch(() => '');
|
|
console.log(`[TEST3] Clicking: "${spBtnText.trim()}"`);
|
|
await fortsaetSP.first().click();
|
|
await page.waitForTimeout(3000);
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-04-smart-pakke-step.png'), fullPage: true });
|
|
|
|
// Now on Smart Pakke step, look for "Klar til Final Review" or "Videre til Final Review"
|
|
const bodyAfterSP = await page.locator('body').textContent();
|
|
console.log(`[TEST3] After Smart Pakke step, page contains "Final Review": ${bodyAfterSP.includes('Final Review')}`);
|
|
console.log(`[TEST3] After Smart Pakke step, page contains "Tilbudstekst": ${bodyAfterSP.includes('Tilbudstekst')}`);
|
|
|
|
// Look for button to go to Final Review from Smart Pakke step
|
|
const toFinalBtns = [
|
|
page.locator('button').filter({ hasText: /Final Review/i }),
|
|
page.locator('button').filter({ hasText: /Videre|Forts.*t|Gennemse/i }),
|
|
page.locator('button[title*="Final Review"]'),
|
|
];
|
|
|
|
for (const btn of toFinalBtns) {
|
|
const count = await btn.count().catch(() => 0);
|
|
for (let i = 0; i < count; i++) {
|
|
const visible = await btn.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const text = await btn.nth(i).textContent().catch(() => '');
|
|
// Skip nav buttons
|
|
if (text.match(/Projekt Flow|Planl.*gning|Materialer|Smart Pakker & Opgaver|AI Budget/i)) continue;
|
|
console.log(`[TEST3] Clicking to advance: "${text.trim().substring(0, 80)}"`);
|
|
await btn.nth(i).click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try clicking Final Review step nav again
|
|
const frStep2 = page.locator('.step, [class*="step"]').filter({ hasText: /Final Review/i });
|
|
if (await frStep2.first().isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const frClass2 = await frStep2.first().getAttribute('class').catch(() => '');
|
|
console.log(`[TEST3] Final Review step class (2nd check): "${frClass2}"`);
|
|
if (!frClass2.includes('disabled')) {
|
|
await frStep2.first().click();
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
const bodyNow = await page.locator('body').textContent();
|
|
reachedFinalReview = bodyNow.includes('Tilbudstekst') || bodyNow.includes('Final Review - Tilbud');
|
|
console.log(`[TEST3] Reached Final Review (2nd attempt): ${reachedFinalReview}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-05-final-review.png'), fullPage: true });
|
|
|
|
// Step 6: Look for AI text generation
|
|
console.log('[TEST3] Step 6: Looking for AI text generation...');
|
|
|
|
// List ALL visible buttons
|
|
const allButtons = page.locator('button:visible');
|
|
const btnCount = await allButtons.count();
|
|
console.log(`[TEST3] All visible buttons (${btnCount}):`);
|
|
for (let i = 0; i < Math.min(btnCount, 40); i++) {
|
|
const text = await allButtons.nth(i).textContent().catch(() => '');
|
|
if (text.trim()) {
|
|
console.log(`[TEST3] Button[${i}]: "${text.trim().substring(0, 100)}"`);
|
|
}
|
|
}
|
|
|
|
// Check for radio buttons to select AI mode
|
|
const aiRadio = page.locator('text=/AI Genereret Tilbudstekst/i');
|
|
if (await aiRadio.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
console.log('[TEST3] Found "AI Genereret Tilbudstekst" radio option, clicking...');
|
|
await aiRadio.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Search for the "Generer" button
|
|
const genererBtn = page.locator('button').filter({ hasText: /Generer.*Tilbudstekst/i });
|
|
const genererCount = await genererBtn.count().catch(() => 0);
|
|
console.log(`[TEST3] "Generer Tilbudstekst" buttons found: ${genererCount}`);
|
|
|
|
let aiButton = null;
|
|
for (let i = 0; i < genererCount; i++) {
|
|
const visible = await genererBtn.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const text = await genererBtn.nth(i).textContent().catch(() => '');
|
|
console.log(`[TEST3] Found AI generation button: "${text.trim()}"`);
|
|
aiButton = genererBtn.nth(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback: look for any "Generer" button
|
|
if (!aiButton) {
|
|
const anyGenerer = page.locator('button').filter({ hasText: /Generer/i });
|
|
const anyCount = await anyGenerer.count().catch(() => 0);
|
|
for (let i = 0; i < anyCount; i++) {
|
|
const visible = await anyGenerer.nth(i).isVisible().catch(() => false);
|
|
const text = await anyGenerer.nth(i).textContent().catch(() => '');
|
|
if (visible && !text.match(/AI Budget/i)) {
|
|
console.log(`[TEST3] Fallback - found "Generer" button: "${text.trim()}"`);
|
|
aiButton = anyGenerer.nth(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (aiButton) {
|
|
// Step 7: Click AI generation and capture response
|
|
console.log('[TEST3] Step 7: Clicking AI generation button...');
|
|
|
|
// Set up response listener for generate-quote API
|
|
const aiResponsePromise = page.waitForResponse(
|
|
resp => resp.url().includes('/api/') && (
|
|
resp.url().includes('generate') ||
|
|
resp.url().includes('quote') ||
|
|
resp.url().includes('tilbud')
|
|
) && resp.request().method() === 'POST',
|
|
{ timeout: 60000 }
|
|
).catch(() => null);
|
|
|
|
await aiButton.click();
|
|
console.log('[TEST3] Waiting for AI API response (up to 60s)...');
|
|
|
|
// Also watch for any response
|
|
await page.waitForTimeout(3000);
|
|
|
|
const aiResponse = await aiResponsePromise;
|
|
if (aiResponse) {
|
|
console.log(`[TEST3] AI API Response: ${aiResponse.status()} ${aiResponse.statusText()}`);
|
|
console.log(`[TEST3] AI API URL: ${aiResponse.url()}`);
|
|
console.log(`[TEST3] AI API Method: ${aiResponse.request().method()}`);
|
|
|
|
try {
|
|
const responseBody = await aiResponse.text();
|
|
console.log(`[TEST3] AI API Response Body (first 1000 chars): ${responseBody.substring(0, 1000)}`);
|
|
|
|
try {
|
|
const json = JSON.parse(responseBody);
|
|
if (json.error) console.log(`[TEST3] AI API ERROR: ${JSON.stringify(json.error)}`);
|
|
if (json.success === false) console.log(`[TEST3] AI API returned success=false`);
|
|
if (json.quoteText) console.log(`[TEST3] AI generated text (first 300): ${json.quoteText.substring(0, 300)}`);
|
|
} catch (e) { /* not JSON */ }
|
|
} catch (e) {
|
|
console.log(`[TEST3] Could not read AI response body: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log('[TEST3] No matching AI API response detected within 60 seconds');
|
|
// Check all API calls that happened
|
|
console.log('[TEST3] Recent API calls after click:');
|
|
monitoring.allApiCalls.slice(-10).forEach(c => {
|
|
console.log(`[TEST3] ${c.status} ${c.url.substring(0, 120)}`);
|
|
});
|
|
}
|
|
|
|
await page.waitForTimeout(5000);
|
|
} else {
|
|
console.log('[TEST3] NO AI GENERATION BUTTON FOUND on this page');
|
|
|
|
if (!reachedFinalReview) {
|
|
console.log('[TEST3] REASON: Did not successfully navigate to Final Review step');
|
|
console.log('[TEST3] Final Review requires: packageData AND (geometry OR enhancedGeometry)');
|
|
console.log('[TEST3] The project may not have Smart Pakke data configured');
|
|
}
|
|
|
|
// Check what page we're actually on
|
|
const bodyText = await page.locator('body').textContent();
|
|
const currentPageHints = bodyText.match(/(Geometri|Smart Pakke|Final Review|Projekt|Tilbudstekst)[^A-Z]{0,50}/gi);
|
|
if (currentPageHints) {
|
|
currentPageHints.slice(0, 10).forEach(h => console.log(`[TEST3] Current page hint: "${h.trim()}"`));
|
|
}
|
|
}
|
|
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-06-after-ai-action.png'), fullPage: true });
|
|
|
|
// Step 8: Capture all errors and final state
|
|
console.log('[TEST3] Step 8: Final state capture...');
|
|
|
|
// Check textareas for generated content
|
|
const textAreas = page.locator('textarea:visible');
|
|
const taCount = await textAreas.count().catch(() => 0);
|
|
console.log(`[TEST3] Visible textareas: ${taCount}`);
|
|
for (let i = 0; i < taCount; i++) {
|
|
const value = await textAreas.nth(i).inputValue().catch(() => '');
|
|
const placeholder = await textAreas.nth(i).getAttribute('placeholder').catch(() => '');
|
|
console.log(`[TEST3] Textarea[${i}] placeholder="${placeholder}" len=${value.length} content="${value.substring(0, 200)}"`);
|
|
}
|
|
|
|
// Check for toasts/notifications
|
|
const toasts = page.locator('[class*="toast"], [class*="Toast"], [class*="Toastify"], [class*="notification"]');
|
|
const toastCount = await toasts.count().catch(() => 0);
|
|
for (let i = 0; i < toastCount; i++) {
|
|
const visible = await toasts.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const toastText = await toasts.nth(i).textContent().catch(() => '');
|
|
console.log(`[TEST3] TOAST/NOTIFICATION: "${toastText.trim().substring(0, 300)}"`);
|
|
}
|
|
}
|
|
|
|
// Check for alerts
|
|
const alerts = page.locator('[role="alert"], .MuiAlert-root');
|
|
const alertCount = await alerts.count().catch(() => 0);
|
|
for (let i = 0; i < alertCount; i++) {
|
|
const visible = await alerts.nth(i).isVisible().catch(() => false);
|
|
if (visible) {
|
|
const alertText = await alerts.nth(i).textContent().catch(() => '');
|
|
console.log(`[TEST3] ALERT: "${alertText.trim().substring(0, 300)}"`);
|
|
}
|
|
}
|
|
|
|
reportFindings('TEST 3: AI Quote Text Generation', monitoring);
|
|
await page.screenshot({ path: path.join(ARTIFACTS_DIR, 'test3-07-final-state.png'), fullPage: true });
|
|
});
|
|
});
|