// @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 }); }); });