Files
tilbudgivern/tests/lars-carpenter-test.spec.js

1023 lines
45 KiB
JavaScript

// @ts-check
/**
* Lars's Comprehensive User Testing Suite for Tilbudgivern
* Perspective: Danish master carpenter (tømrermester), 47 years old
* 22 years in business, specializes in roofing (tagarbejde), 3 employees
*
* This test suite evaluates ALL major flows from a real carpenter's daily workflow.
*/
const { test, expect } = require('@playwright/test');
const path = require('path');
const fs = require('fs');
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'https://tilbudsgiveren.alw.dk';
const SCREENSHOT_DIR = '/mnt/HC_Volume_103713257/tilbudgivern/screenshots/lars-test';
// Ensure screenshot directory exists
if (!fs.existsSync(SCREENSHOT_DIR)) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
}
const RESULTS = {
flows: {}
};
async function screenshot(page, name, notes = '') {
const filePath = path.join(SCREENSHOT_DIR, `${name}.png`);
await page.screenshot({ path: filePath, fullPage: true });
console.log(`[SCREENSHOT] ${name}.png${notes ? ' - ' + notes : ''}`);
return filePath;
}
async function loginToApp(page) {
await page.goto(BASE_URL + '/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
// Check if already logged in
const hasPasswordField = await page.locator('input[type="password"]').isVisible({ timeout: 3000 }).catch(() => false);
if (!hasPasswordField) {
console.log('[AUTH] Already logged in or no login required');
return true;
}
// Fill login form
await page.locator('input[type="text"]').first().fill('toemrer');
await page.locator('input[type="password"]').first().fill('tilbud2024');
await page.waitForTimeout(300);
// Try the submit button
const loginBtn = page.getByRole('button', { name: /log ind/i });
if (await loginBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await loginBtn.click();
} else {
await page.keyboard.press('Enter');
}
await page.waitForTimeout(3000);
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
const stillOnLogin = await page.locator('input[type="password"]').isVisible({ timeout: 2000 }).catch(() => false);
if (stillOnLogin) {
console.log('[AUTH] Login may have failed, trying again...');
return false;
}
console.log('[AUTH] Login successful');
return true;
}
// Wait for React app to fully render
async function waitForApp(page) {
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
// Wait until body has substantial content (React has rendered)
await page.waitForFunction(() => {
const body = document.body.textContent || '';
return body.length > 100 && !body.includes('Du skal aktivere JavaScript');
}, { timeout: 15000 }).catch(() => {});
}
// ============================================================
// TEST 1: LOGIN FLOW
// ============================================================
test.describe('Lars User Testing Suite', () => {
test.setTimeout(300000); // 5 minutes per test
test('FLOW 1 - Login og første indtryk', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 1: LOGIN OG FØRSTE INDTRYK');
console.log('════════════════════════════════════════');
// Navigate to the app
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
await screenshot(page, '01_01_initial_load', 'First look at the app');
// Check login page elements
const pageTitle = await page.title();
console.log(`[LOGIN] Page title: "${pageTitle}"`);
// Look at what's on the login page
const bodyText = await page.textContent('body');
console.log(`[LOGIN] Page content: "${bodyText.substring(0, 200)}"`);
// Check for branding
const hasLogo = bodyText.includes('Tilbudgivern') || bodyText.includes('Tilbudsgivern');
console.log(`[LOGIN] Has app name/branding: ${hasLogo}`);
// Check form fields
const usernameField = page.locator('input[type="text"]').first();
const passwordField = page.locator('input[type="password"]').first();
const loginButton = page.getByRole('button', { name: /log ind/i });
const hasUsername = await usernameField.isVisible({ timeout: 3000 }).catch(() => false);
const hasPassword = await passwordField.isVisible({ timeout: 3000 }).catch(() => false);
const hasLoginBtn = await loginButton.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[LOGIN] Username field visible: ${hasUsername}`);
console.log(`[LOGIN] Password field visible: ${hasPassword}`);
console.log(`[LOGIN] Login button visible: ${hasLoginBtn}`);
// Check for placeholder text / labels
const labels = await page.locator('label').all();
console.log(`[LOGIN] Form labels:`);
for (const label of labels) {
const text = await label.textContent();
console.log(` - "${text?.trim()}"`);
}
// Check input placeholders
const inputs = await page.locator('input').all();
for (const input of inputs) {
const type = await input.getAttribute('type');
const placeholder = await input.getAttribute('placeholder');
console.log(`[LOGIN] Input[type=${type}] placeholder="${placeholder}"`);
}
// Attempt login
if (hasUsername && hasPassword) {
await usernameField.fill('toemrer');
await passwordField.fill('tilbud2024');
await screenshot(page, '01_02_login_filled', 'Credentials entered');
if (hasLoginBtn) {
await loginButton.click();
} else {
await page.keyboard.press('Enter');
}
await page.waitForTimeout(3000);
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
await waitForApp(page);
await screenshot(page, '01_03_after_login', 'After login attempt');
const currentUrl = page.url();
const loginSuccess = !currentUrl.includes('login') && !(await page.locator('input[type="password"]').isVisible({ timeout: 2000 }).catch(() => false));
console.log(`[LOGIN] Login result - URL: ${currentUrl}, Success: ${loginSuccess}`);
// Capture the main app landing page
const appContent = await page.textContent('body');
console.log(`[LOGIN] App landing content (first 500 chars): "${appContent.substring(0, 500)}"`);
// Check navigation elements
const navLinks = await page.locator('nav a, [role="navigation"] a, .MuiDrawer-root a, [class*="sidebar"] a').all();
console.log(`[LOGIN] Navigation links found: ${navLinks.length}`);
for (const link of navLinks) {
const text = await link.textContent();
const href = await link.getAttribute('href');
console.log(` Nav: "${text?.trim()}" -> ${href}`);
}
// Find all clickable navigation
const allTabs = await page.locator('[role="tab"], [class*="tab"]').all();
console.log(`[LOGIN] Tabs found: ${allTabs.length}`);
for (const tab of allTabs) {
const text = await tab.textContent();
console.log(` Tab: "${text?.trim()}"`);
}
expect(loginSuccess).toBe(true);
}
await screenshot(page, '01_04_logged_in_home', 'Home page after login');
console.log('[FLOW 1] DONE');
});
// ============================================================
// TEST 2: CREATE NEW PROJECT
// ============================================================
test('FLOW 2 - Opret nyt projekt', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 2: OPRET NYT PROJEKT');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
await screenshot(page, '02_01_home', 'Home after login');
// Examine the main app structure
const bodyText = await page.textContent('body');
console.log(`[PROJECT] Home page content: "${bodyText.substring(0, 800)}"`);
// Find all buttons on the main page
const allButtons = await page.locator('button:visible').all();
console.log(`[PROJECT] Visible buttons on home:`);
for (const btn of allButtons) {
const text = await btn.textContent();
const disabled = await btn.isDisabled();
if (text?.trim()) console.log(` Btn: "${text.trim()}" disabled=${disabled}`);
}
// Look for "Nyt Projekt" button
const nytProjektBtn = page.getByRole('button', { name: /nyt projekt/i });
const hasNytProjekt = await nytProjektBtn.isVisible({ timeout: 5000 }).catch(() => false);
console.log(`[PROJECT] "Nyt Projekt" button visible: ${hasNytProjekt}`);
if (hasNytProjekt) {
await nytProjektBtn.click();
await page.waitForTimeout(1000);
await screenshot(page, '02_02_nyt_projekt_clicked', 'After clicking Nyt Projekt');
}
// Check for project form
await waitForApp(page);
const formContent = await page.textContent('body');
console.log(`[PROJECT] Form content: "${formContent.substring(0, 1000)}"`);
// List all visible labels in the form
const allLabels = await page.locator('label:visible').all();
console.log(`[PROJECT] Form labels:`);
for (const label of allLabels) {
const text = await label.textContent();
const forAttr = await label.getAttribute('for');
console.log(` Label: "${text?.trim()}" for="${forAttr}"`);
}
// List all visible inputs
const allInputs = await page.locator('input:visible, textarea:visible').all();
console.log(`[PROJECT] Form inputs: ${allInputs.length}`);
for (const input of allInputs) {
const type = await input.getAttribute('type');
const id = await input.getAttribute('id');
const name = await input.getAttribute('name');
const placeholder = await input.getAttribute('placeholder');
const labelText = await page.locator(`label[for="${id}"]`).textContent().catch(() => '');
console.log(` Input[type=${type}, id=${id}] label="${labelText}" placeholder="${placeholder}"`);
}
// Fill project form - realistic carpenter scenario
// Project name field
const projectNameInput = page.locator('#field-projectName');
if (await projectNameInput.isVisible({ timeout: 3000 }).catch(() => false)) {
await projectNameInput.fill('Tagpap udskiftning - Møllevej 14, Horsens');
console.log('[PROJECT] Filled project name via #field-projectName');
} else {
// Fallback
const firstTextInput = page.locator('input[type="text"]:visible').first();
if (await firstTextInput.isVisible().catch(() => false)) {
await firstTextInput.fill('Tagpap udskiftning - Møllevej 14, Horsens');
console.log('[PROJECT] Filled project name via first text input');
}
}
// Customer name
const customerNameInput = page.locator('#field-customerName');
if (await customerNameInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await customerNameInput.fill('Jens Hansen');
console.log('[PROJECT] Filled customer name');
}
// Email
const emailInput = page.locator('#field-customerEmail, input[type="email"]').first();
if (await emailInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await emailInput.fill('jens.hansen@gmail.com');
console.log('[PROJECT] Filled email');
}
// Address
const addressInput = page.locator('#field-address, input[placeholder*="adresse" i], input[placeholder*="vejnavn" i]').first();
if (await addressInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await addressInput.fill('Møllevej 14, 8700 Horsens');
console.log('[PROJECT] Filled address');
}
// Description - the realistic project description Lars would enter
const descInput = page.locator('#field-projectDescription, textarea:visible').first();
if (await descInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await descInput.fill('Udskiftning af tagpap på enfamilieshus, ca. 120 m2 fladt tag. Eksisterende tagpap er gammel og utæt, skal fjernes og erstattes med ny tagpap med 2 lag. Inkl. ny tagbrønde og tætning ved gennemføringer.');
console.log('[PROJECT] Filled description');
}
await screenshot(page, '02_03_form_filled', 'Project form filled in');
// Check for any tooltips or help text
const helperTexts = await page.locator('[class*="helper"], [class*="hint"], [class*="Helper"]').all();
console.log(`[PROJECT] Helper texts: ${helperTexts.length}`);
for (const helper of helperTexts) {
const text = await helper.textContent();
if (text?.trim()) console.log(` Helper: "${text.trim()}"`);
}
// Find submit / next button
const submitBtns = await page.locator('button:visible').all();
console.log('[PROJECT] Submit/Next buttons:');
for (const btn of submitBtns) {
const text = await btn.textContent();
const disabled = await btn.isDisabled();
if (text?.trim()) console.log(` "${text.trim()}" disabled=${disabled}`);
}
// Click submit / gem projekt
const gemProjektBtn = page.getByRole('button', { name: /gem projekt|opret projekt|næste|fortsæt/i });
const hasGem = await gemProjektBtn.isVisible({ timeout: 3000 }).catch(() => false);
if (hasGem) {
console.log('[PROJECT] Clicking submit button...');
await gemProjektBtn.click();
await page.waitForTimeout(3000);
await waitForApp(page);
await screenshot(page, '02_04_after_submit', 'After project creation');
const afterContent = await page.textContent('body');
console.log(`[PROJECT] After submit content (first 500 chars): "${afterContent.substring(0, 500)}"`);
}
await screenshot(page, '02_05_project_result', 'Final state after project creation');
console.log('[FLOW 2] DONE');
});
// ============================================================
// TEST 3: GEOMETRY STEP
// ============================================================
test('FLOW 3 - Geometri (tagmål)', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 3: GEOMETRI (TAGMÅL)');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
// First create or find an existing project, then navigate to geometry
// Click Nyt Projekt first
const nytProjektBtn = page.getByRole('button', { name: /nyt projekt/i });
if (await nytProjektBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
await nytProjektBtn.click();
await page.waitForTimeout(1000);
}
// Fill minimum required fields
const projectNameInput = page.locator('#field-projectName');
if (await projectNameInput.isVisible({ timeout: 3000 }).catch(() => false)) {
await projectNameInput.fill('Lars Test - Geometri');
}
const customerNameInput = page.locator('#field-customerName');
if (await customerNameInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await customerNameInput.fill('Test Kunde');
}
const emailInput = page.locator('#field-customerEmail, input[type="email"]').first();
if (await emailInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await emailInput.fill('test@test.dk');
}
const descInput = page.locator('#field-projectDescription, textarea:visible').first();
if (await descInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await descInput.fill('Fladtag 120 m2 tagpap');
}
// Submit to get to geometry
const gemBtn = page.getByRole('button', { name: /gem projekt/i });
if (await gemBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await gemBtn.click();
await page.waitForTimeout(4000);
await waitForApp(page);
}
await screenshot(page, '03_01_after_project_create', 'After project creation - should be on geometry');
// Check if we're on geometry step
const bodyText = await page.textContent('body');
console.log(`[GEOMETRY] Page content after project create: "${bodyText.substring(0, 1000)}"`);
// Look for roof type selector
const roofTypeElements = await page.locator('select, [role="combobox"], [class*="tagtype"], [class*="rooftype"]').all();
console.log(`[GEOMETRY] Roof type selectors: ${roofTypeElements.length}`);
// Check for geometry labels
const geometryKeywords = {
'bredde': 'Width',
'længde': 'Length',
'hældning': 'Pitch/Slope',
'grader': 'Degrees',
'areal': 'Area',
'tagareal': 'Roof area',
'm2': 'Square meters',
'tagtype': 'Roof type',
'sadeltag': 'Saddle roof',
'fladtag': 'Flat roof',
'valmtag': 'Hip roof',
'rygning': 'Ridge',
'facade': 'Facade',
'spær': 'Rafters',
};
console.log('[GEOMETRY] Keywords found on page:');
for (const [kw, meaning] of Object.entries(geometryKeywords)) {
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
console.log(` ✓ "${kw}" (${meaning})`);
} else {
console.log(` ✗ "${kw}" (${meaning}) - NOT FOUND`);
}
}
// Find all numeric inputs (typical for dimensions)
const numInputs = await page.locator('input[type="number"]:visible').all();
console.log(`[GEOMETRY] Numeric inputs: ${numInputs.length}`);
for (const input of numInputs) {
const placeholder = await input.getAttribute('placeholder');
const id = await input.getAttribute('id');
const name = await input.getAttribute('name');
// Find associated label
const labelEl = page.locator(`label[for="${id}"]`);
const labelText = await labelEl.textContent().catch(() => '');
console.log(` NumInput[id=${id}, placeholder=${placeholder}] label="${labelText}"`);
}
// Try to fill in geometry fields
// Bredde (width)
const breddeByPlaceholder = page.locator('input[type="number"][placeholder*="10" i], input[type="number"][placeholder*="bredde" i]').first();
const breddeInput = page.locator('label').filter({ hasText: /bredde/i }).locator('xpath=following-sibling::*//input, following::input').first();
// Try multiple approaches to find width input
let filledBredde = false;
const numInputsList = await page.locator('input[type="number"]:visible').all();
if (numInputsList.length >= 1) {
await numInputsList[0].fill('8');
console.log('[GEOMETRY] Filled first numeric input (bredde?) with 8');
filledBredde = true;
}
if (numInputsList.length >= 2) {
await numInputsList[1].fill('15');
console.log('[GEOMETRY] Filled second numeric input (længde?) with 15');
}
if (numInputsList.length >= 3) {
await numInputsList[2].fill('30');
console.log('[GEOMETRY] Filled third numeric input (hældning?) with 30');
}
await screenshot(page, '03_02_geometry_inputs', 'Geometry inputs - before calculate');
// Look for calculate button
const calcBtn = page.getByRole('button', { name: /beregn|beregn areal|calculate|gem nu|opdater/i });
const hasCalc = await calcBtn.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[GEOMETRY] Calculate button visible: ${hasCalc}`);
if (hasCalc) {
await calcBtn.click();
await page.waitForTimeout(3000);
await waitForApp(page);
await screenshot(page, '03_03_after_calculate', 'After area calculation');
const calcResult = await page.textContent('body');
const hasAreaResult = calcResult.includes('m²') || calcResult.includes('m2');
console.log(`[GEOMETRY] Area result shown (m²): ${hasAreaResult}`);
// Find calculated area value
const areaMatches = calcResult.match(/(\d+[.,]?\d*)\s*m[²2]/g);
if (areaMatches) {
console.log(`[GEOMETRY] Area values found: ${areaMatches.join(', ')}`);
}
}
// Check for visual SVG/diagram of roof
const svgElements = await page.locator('svg').all();
console.log(`[GEOMETRY] SVG visualizations: ${svgElements.length}`);
// Check for roof type options
const roofOptions = await page.locator('[class*="roofType"], [class*="tagtype"], [data-testid*="roof"]').all();
console.log(`[GEOMETRY] Roof type option elements: ${roofOptions.length}`);
// Look for specific roof type buttons/selectors
const roofTypeButtons = await page.locator('button, [role="option"], [class*="chip"]').filter({ hasText: /sadeltag|fladtag|valmtag|mansard|pult|kviste|københavner/i }).all();
console.log(`[GEOMETRY] Roof type buttons: ${roofTypeButtons.length}`);
for (const btn of roofTypeButtons) {
const text = await btn.textContent();
console.log(` Roof type: "${text?.trim()}"`);
}
// Try to find "Fortsæt" button to move to next step
const fortsætBtn = page.getByRole('button', { name: /fortsæt|næste|spring over/i });
const hasFortsæt = await fortsætBtn.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[GEOMETRY] "Fortsæt/Næste" button visible: ${hasFortsæt}`);
await screenshot(page, '03_04_geometry_complete', 'Geometry step complete');
console.log('[FLOW 3] DONE');
});
// ============================================================
// TEST 4: SMART PACKAGES
// ============================================================
test('FLOW 4 - Smart Pakke valg', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 4: SMART PAKKE VALG');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
// Create a project first to reach smart pakke step
const nytProjektBtn = page.getByRole('button', { name: /nyt projekt/i });
if (await nytProjektBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
await nytProjektBtn.click();
await page.waitForTimeout(500);
}
const projectName = page.locator('#field-projectName');
if (await projectName.isVisible({ timeout: 3000 }).catch(() => false)) {
await projectName.fill('Lars Smart Pakke Test');
}
const customerName = page.locator('#field-customerName');
if (await customerName.isVisible({ timeout: 2000 }).catch(() => false)) {
await customerName.fill('Peter Andersen');
}
const email = page.locator('#field-customerEmail, input[type="email"]').first();
if (await email.isVisible({ timeout: 2000 }).catch(() => false)) {
await email.fill('peter@andersen.dk');
}
const desc = page.locator('#field-projectDescription, textarea:visible').first();
if (await desc.isVisible({ timeout: 2000 }).catch(() => false)) {
await desc.fill('Nyt sadeltag med teglsten, 180 m2');
}
const gemBtn = page.getByRole('button', { name: /gem projekt/i });
if (await gemBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await gemBtn.click();
await page.waitForTimeout(4000);
await waitForApp(page);
}
// If on geometry step, fill it minimally and continue
const numInputsOnGeom = await page.locator('input[type="number"]:visible').all();
if (numInputsOnGeom.length > 0) {
for (const input of numInputsOnGeom) {
await input.fill('10');
}
const gemNuBtn = page.getByRole('button', { name: /gem nu|beregn|fortsæt/i });
if (await gemNuBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await gemNuBtn.click();
await page.waitForTimeout(4000);
await waitForApp(page);
}
}
// Try to skip geometry or proceed to smart pakke
const skipBtn = page.getByRole('button', { name: /spring over|hop over|skip/i });
if (await skipBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await skipBtn.click();
await page.waitForTimeout(2000);
}
const fortsætBtn = page.getByRole('button', { name: /fortsæt til smart pakke|fortsæt/i });
if (await fortsætBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
await fortsætBtn.click();
await page.waitForTimeout(3000);
await waitForApp(page);
}
await screenshot(page, '04_01_smart_pakke_step', 'Smart Pakke step');
const bodyText = await page.textContent('body');
console.log(`[SMART PAKKE] Page content: "${bodyText.substring(0, 1500)}"`);
// Look for package options
const packageOptions = await page.locator('.package-option, [class*="packageOption"], [class*="pakke"]').all();
console.log(`[SMART PAKKE] Package option elements: ${packageOptions.length}`);
// Look for package names/cards
const packageCards = await page.locator('[class*="card"], [class*="Card"]').all();
console.log(`[SMART PAKKE] Card elements: ${packageCards.length}`);
// List all headings to find package names
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
console.log('[SMART PAKKE] Headings on page:');
for (const heading of headings) {
const text = await heading.textContent();
if (text?.trim()) console.log(` "${text.trim()}"`);
}
// Check for specific package types relevant for tømrer
const packageKeywords = [
'tagsten', 'tagpap', 'tegl', 'betontagsten', 'fibercementtagsten',
'tagrende', 'nedløb', 'tagvinduer', 'vindskede', 'sternbræt',
'undertag', 'spær', 'isol', 'tagbrønd'
];
console.log('[SMART PAKKE] Relevant material keywords:');
for (const kw of packageKeywords) {
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
console.log(` ✓ "${kw}"`);
}
}
// Find material count information
const materialCount = bodyText.match(/(\d+)\s*materiale/i);
if (materialCount) console.log(`[SMART PAKKE] Material count: ${materialCount[0]}`);
// Look for labor descriptions
const laborKeywords = ['timer', 'mandetimer', 'opgave', 'timeløn', 'arbejdstime'];
console.log('[SMART PAKKE] Labor keywords:');
for (const kw of laborKeywords) {
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
console.log(` ✓ "${kw}"`);
}
}
// Try to select a package
const firstPackage = page.locator('.package-option').first();
if (await firstPackage.isVisible({ timeout: 3000 }).catch(() => false)) {
console.log('[SMART PAKKE] Clicking first package option...');
await firstPackage.click();
await page.waitForTimeout(3000);
await waitForApp(page);
await screenshot(page, '04_02_package_selected', 'After package selection');
const afterSelect = await page.textContent('body');
console.log(`[SMART PAKKE] After selection: "${afterSelect.substring(0, 800)}"`);
// Check for material list after selection
const materialItems = await page.locator('[class*="material"], [class*="Material"]').all();
console.log(`[SMART PAKKE] Material items visible: ${materialItems.length}`);
} else {
// Try MUI select or dropdown
const selectElements = await page.locator('select:visible, [role="listbox"]:visible').all();
console.log(`[SMART PAKKE] Select elements: ${selectElements.length}`);
}
await screenshot(page, '04_03_smart_pakke_overview', 'Smart Pakke overview');
console.log('[FLOW 4] DONE');
});
// ============================================================
// TEST 5 & 6: FINAL REVIEW + AI TILBUDSTEKST (combined)
// ============================================================
test('FLOW 5+6 - Final Review og AI Tilbudstekst', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 5+6: FINAL REVIEW OG AI TILBUDSTEKST');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
await screenshot(page, '05_01_home', 'Home page for final review test');
// Look for existing projects to review
const existingProjectLinks = await page.locator('a[href*="/project"], a[href*="/projekt"], [class*="project-item"], [class*="projectItem"]').all();
console.log(`[FINAL REVIEW] Existing project links: ${existingProjectLinks.length}`);
if (existingProjectLinks.length > 0) {
const linkText = await existingProjectLinks[0].textContent();
console.log(`[FINAL REVIEW] Clicking first project: "${linkText?.trim()}"`);
await existingProjectLinks[0].click();
await page.waitForTimeout(2000);
await waitForApp(page);
await screenshot(page, '05_02_existing_project', 'Existing project page');
} else {
// Try looking for projects in a list
const projectRows = await page.locator('[class*="projektRow"], [class*="project-row"], tr').all();
console.log(`[FINAL REVIEW] Project rows: ${projectRows.length}`);
}
// Check current page content
const bodyText = await page.textContent('body');
console.log(`[FINAL REVIEW] Page content: "${bodyText.substring(0, 1000)}"`);
// Navigate to Final Review step if possible
const finalReviewStep = page.locator('[class*="step"]').filter({ hasText: /final review|tilbudstekst|gennemse/i });
if (await finalReviewStep.isVisible({ timeout: 3000 }).catch(() => false)) {
await finalReviewStep.click();
await page.waitForTimeout(2000);
}
await screenshot(page, '05_03_final_review_page', 'Final Review page');
// Examine final review content
const reviewContent = await page.textContent('body');
// Check for price elements
const priceKeywords = {
'kr.': 'Danish price format',
'kr,': 'Danish price format alt',
',00': 'Price with decimals',
'moms': 'VAT (25%)',
'total': 'Total price',
'subtotal': 'Subtotal',
'pris': 'Price',
'tilbud': 'Quote/Offer',
};
console.log('[FINAL REVIEW] Price-related elements:');
for (const [kw, meaning] of Object.entries(priceKeywords)) {
if (reviewContent.toLowerCase().includes(kw.toLowerCase())) {
console.log(` ✓ "${kw}" (${meaning})`);
}
}
// Check for line items
const lineItems = await page.locator('[class*="lineItem"], [class*="line-item"], tr').all();
console.log(`[FINAL REVIEW] Line items: ${lineItems.length}`);
// Check for action buttons (PDF, send, ordrestyring)
const actionBtns = await page.locator('button:visible').all();
console.log('[FINAL REVIEW] Action buttons:');
for (const btn of actionBtns) {
const text = await btn.textContent();
const disabled = await btn.isDisabled();
if (text?.trim()) console.log(` "${text.trim()}" disabled=${disabled}`);
}
// Check for AI quote generation
const aiGenBtn = page.getByRole('button', { name: /generer.*tilbudstekst|generer ai|ai tilbud/i });
const hasAiBtn = await aiGenBtn.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[AI TILBUD] AI generate button visible: ${hasAiBtn}`);
// Check for radio buttons (manual vs AI)
const radioButtons = await page.locator('input[type="radio"]').all();
console.log(`[AI TILBUD] Radio buttons: ${radioButtons.length}`);
for (const radio of radioButtons) {
const value = await radio.getAttribute('value');
const checked = await radio.isChecked();
console.log(` Radio[value=${value}] checked=${checked}`);
}
// Check for textarea for quote text
const quoteTextarea = page.locator('.quote-textarea, textarea[placeholder*="tilbud" i], textarea[placeholder*="tekst" i]');
const hasQuoteTextarea = await quoteTextarea.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[AI TILBUD] Quote textarea visible: ${hasQuoteTextarea}`);
if (hasAiBtn) {
console.log('[AI TILBUD] Attempting to generate AI quote...');
await aiGenBtn.click();
await page.waitForTimeout(5000);
await screenshot(page, '05_04_generating_ai', 'AI generation in progress');
// Wait for generation
let elapsed = 0;
let generating = true;
while (generating && elapsed < 60000) {
await page.waitForTimeout(3000);
elapsed += 3000;
const spinnerVisible = await page.locator('[class*="spinner"], [class*="loading"], [aria-label*="loading"]').isVisible({ timeout: 1000 }).catch(() => false);
const genBtnText = await page.getByRole('button', { name: /genererer/i }).isVisible({ timeout: 1000 }).catch(() => false);
generating = spinnerVisible || genBtnText;
if (elapsed % 9000 === 0) console.log(`[AI TILBUD] Generating... ${elapsed / 1000}s`);
}
await screenshot(page, '05_05_after_ai_generation', 'After AI generation');
const generatedText = await page.textContent('body');
const hasLongText = generatedText.length > 5000;
console.log(`[AI TILBUD] Page content length after generation: ${generatedText.length}`);
// Try to get quote text content
if (hasQuoteTextarea) {
const textareaValue = await quoteTextarea.inputValue().catch(() => '');
console.log(`[AI TILBUD] Quote text length: ${textareaValue.length}`);
if (textareaValue.length > 50) {
console.log(`[AI TILBUD] Quote preview: "${textareaValue.substring(0, 400)}"`);
}
}
}
// Check for Ordrestyring integration button
const ordrestyringBtn = page.getByRole('button', { name: /ordrestyring|send tilbud|opret i ordrestyring/i });
const hasOrdrestyring = await ordrestyringBtn.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[FINAL REVIEW] Ordrestyring button visible: ${hasOrdrestyring}`);
// Check for PDF button
const pdfBtn = page.getByRole('button', { name: /pdf|download|hent/i });
const hasPdf = await pdfBtn.isVisible({ timeout: 3000 }).catch(() => false);
console.log(`[FINAL REVIEW] PDF button visible: ${hasPdf}`);
await screenshot(page, '05_06_final_review_complete', 'Final review complete');
console.log('[FLOW 5+6] DONE');
});
// ============================================================
// TEST 7: PLANNING DASHBOARD
// ============================================================
test('FLOW 7 - Planlægningsdashboard', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 7: PLANLÆGNINGSDASHBOARD');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
await screenshot(page, '07_01_home', 'Home page for planning test');
// Find planning/calendar link
const planningNavItem = page.locator('a, button, [role="tab"]').filter({ hasText: /planlæ|planning|kalender|calendar/i }).first();
const hasPlanningNav = await planningNavItem.isVisible({ timeout: 5000 }).catch(() => false);
console.log(`[PLANNING] Planning nav item visible: ${hasPlanningNav}`);
if (hasPlanningNav) {
const planText = await planningNavItem.textContent();
console.log(`[PLANNING] Planning nav text: "${planText?.trim()}"`);
await planningNavItem.click();
await page.waitForTimeout(2000);
await waitForApp(page);
} else {
// Try direct URL navigation
await page.goto(`${BASE_URL}/planlæggning`, { waitUntil: 'domcontentloaded' }).catch(() => {});
await page.waitForTimeout(2000);
if (page.url().includes('login')) {
await loginToApp(page);
await page.goto(`${BASE_URL}/planning`, { waitUntil: 'domcontentloaded' }).catch(() => {});
await page.waitForTimeout(2000);
}
}
await screenshot(page, '07_02_planning_page', 'Planning dashboard page');
const bodyText = await page.textContent('body');
console.log(`[PLANNING] Planning page content: "${bodyText.substring(0, 1000)}"`);
// Check for calendar elements
const calendarKeywords = [
'kalender', 'calendar', 'mandag', 'tirsdag', 'uge', 'måned', 'dag',
'ordre', 'job', 'opgave', 'medarbejder', 'projekt'
];
console.log('[PLANNING] Calendar keywords:');
for (const kw of calendarKeywords) {
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
console.log(` ✓ "${kw}"`);
}
}
// Look for calendar component
const calElements = await page.locator('[class*="calendar"], [class*="Calendar"], [class*="cal"]').all();
console.log(`[PLANNING] Calendar elements: ${calElements.length}`);
// Check for schedule/event items
const events = await page.locator('[class*="event"], [class*="Event"], [class*="appointment"]').all();
console.log(`[PLANNING] Events/appointments: ${events.length}`);
// Find navigation buttons (prev/next month)
const navBtns = await page.locator('button').filter({ hasText: /i dag|forrige|næste|<|>|←|→/i }).all();
console.log(`[PLANNING] Navigation buttons: ${navBtns.length}`);
for (const btn of navBtns) {
const text = await btn.textContent();
console.log(` "${text?.trim()}"`);
}
// Check for employee scheduling features
const employeeKeywords = ['medarbejder', 'ansat', 'Lars', 'hold'];
for (const kw of employeeKeywords) {
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
console.log(`[PLANNING] Employee feature found: "${kw}"`);
}
}
await screenshot(page, '07_03_planning_detail', 'Planning dashboard detail');
console.log('[FLOW 7] DONE');
});
// ============================================================
// TEST 8: SMART PAKKER MANAGEMENT
// ============================================================
test('FLOW 8 - Smart Pakker administration', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 8: SMART PAKKER ADMINISTRATION');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
await screenshot(page, '08_01_home', 'Home for smart pakker admin test');
// Try to find smart pakker admin - look in main app body for tabs/buttons
const bodyText = await page.textContent('body');
console.log(`[SMART ADMIN] Home content: "${bodyText.substring(0, 500)}"`);
// Look for "Smart Pakker" section in navigation
const smartPakkerNavItems = await page.locator('a, button, [role="tab"]').filter({ hasText: /smart.?pakk|pakker/i }).all();
console.log(`[SMART ADMIN] Smart Pakker nav items: ${smartPakkerNavItems.length}`);
for (const item of smartPakkerNavItems) {
const text = await item.textContent();
const href = await item.getAttribute('href');
console.log(` "${text?.trim()}" href=${href}`);
}
if (smartPakkerNavItems.length > 0) {
await smartPakkerNavItems[0].click();
await page.waitForTimeout(2000);
await waitForApp(page);
await screenshot(page, '08_02_smart_pakker_page', 'Smart Pakker management page');
}
// Also look in the main toggle/stepper area
const toggleBtns = await page.locator('[class*="toggle"], [class*="Toggle"], [role="tab"]').all();
console.log(`[SMART ADMIN] Toggle buttons: ${toggleBtns.length}`);
for (const btn of toggleBtns) {
const text = await btn.textContent();
console.log(` Toggle: "${text?.trim()}"`);
}
// Look for the main dashboard button if exists
const dashboardBtn = page.getByRole('button', { name: /dashboard|oversigt|smart pakk/i });
if (await dashboardBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await dashboardBtn.click();
await page.waitForTimeout(2000);
await waitForApp(page);
}
const adminContent = await page.textContent('body');
console.log(`[SMART ADMIN] Admin page content: "${adminContent.substring(0, 1500)}"`);
// Look for "Create new package" button
const createPkgBtn = await page.locator('button').filter({ hasText: /ny pakke|opret pakke|ny.*pakk|tilføj pakke|create|opret/i }).all();
console.log(`[SMART ADMIN] Create package buttons: ${createPkgBtn.length}`);
for (const btn of createPkgBtn) {
const text = await btn.textContent();
console.log(` Create btn: "${text?.trim()}"`);
}
// Look for existing packages list
const pkgList = await page.locator('[class*="packageList"], [class*="pakkeList"], table').all();
console.log(`[SMART ADMIN] Package list elements: ${pkgList.length}`);
// Count packages visible
const pkgItems = await page.locator('[class*="package-item"], [class*="packageItem"], tbody tr').all();
console.log(`[SMART ADMIN] Package items: ${pkgItems.length}`);
// Try to find and click "Ny Pakke" or similar
const nyPakkeBtn = page.getByRole('button', { name: /ny pakke|opret ny|create new/i });
if (await nyPakkeBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
console.log('[SMART ADMIN] Clicking "Ny Pakke" button...');
await nyPakkeBtn.click();
await page.waitForTimeout(2000);
await waitForApp(page);
await screenshot(page, '08_03_new_package_form', 'New package creation form');
const formContent = await page.textContent('body');
console.log(`[SMART ADMIN] New package form: "${formContent.substring(0, 800)}"`);
}
await screenshot(page, '08_04_smart_admin_final', 'Smart Pakker admin final state');
console.log('[FLOW 8] DONE');
});
// ============================================================
// TEST 9: EXPLORE ALL APP SECTIONS
// ============================================================
test('FLOW 9 - Komplet UI gennemgang', async ({ page }) => {
console.log('\n════════════════════════════════════════');
console.log('FLOW 9: KOMPLET UI GENNEMGANG');
console.log('════════════════════════════════════════');
await loginToApp(page);
await waitForApp(page);
const homeUrl = page.url();
console.log(`[UI] Home URL: ${homeUrl}`);
await screenshot(page, '09_01_full_home', 'Full home page');
// Get complete page structure
const bodyText = await page.textContent('body');
console.log(`\n[UI] FULL PAGE TEXT (2000 chars):\n${bodyText.substring(0, 2000)}`);
// Find ALL clickable elements
const allClickable = await page.locator('button:visible, a:visible, [role="tab"]:visible').all();
console.log(`\n[UI] ALL VISIBLE CLICKABLE ELEMENTS (${allClickable.length}):`);
for (const el of allClickable) {
const tagName = await el.evaluate(el => el.tagName);
const text = await el.textContent();
const href = await el.getAttribute('href');
const role = await el.getAttribute('role');
if (text?.trim()) {
console.log(` <${tagName.toLowerCase()}> "${text.trim()}" href=${href} role=${role}`);
}
}
// Map out the full app structure by clicking each nav item
const mainNavItems = await page.locator('[role="tab"]:visible, nav a:visible, [class*="NavItem"]:visible').all();
console.log(`\n[UI] MAIN NAV ITEMS: ${mainNavItems.length}`);
for (const item of mainNavItems) {
const text = await item.textContent();
console.log(` Nav: "${text?.trim()}"`);
}
// Check the main toggle (Nyt Projekt / Eksisterende)
const toggleGroup = await page.locator('[class*="ToggleButtonGroup"], [class*="toggle-group"]').all();
console.log(`[UI] Toggle groups: ${toggleGroup.length}`);
// Check stepper
const stepperItems = await page.locator('[class*="Step"], [class*="step"], .MuiStep-root').all();
console.log(`[UI] Stepper items: ${stepperItems.length}`);
for (const step of stepperItems) {
const text = await step.textContent();
const classes = await step.getAttribute('class');
console.log(` Step: "${text?.trim().substring(0, 50)}" classes="${classes?.substring(0, 80)}"`);
}
// Look at the existing project view (not new project)
const eksisterendeProjektBtn = page.getByRole('button', { name: /eksisterende projekt|gem.*eksisterende/i });
if (await eksisterendeProjektBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await eksisterendeProjektBtn.click();
await page.waitForTimeout(2000);
await screenshot(page, '09_02_existing_projects', 'Existing projects view');
const projectsContent = await page.textContent('body');
console.log(`[UI] Existing projects view: "${projectsContent.substring(0, 1000)}"`);
}
// Try to find and access Smart Pakker admin section
const tabBarItems = await page.locator('[role="tab"]').all();
console.log(`[UI] Tab bar items: ${tabBarItems.length}`);
for (const tab of tabBarItems) {
const text = await tab.textContent();
const selected = await tab.getAttribute('aria-selected');
console.log(` Tab: "${text?.trim()}" selected=${selected}`);
}
await screenshot(page, '09_03_complete_ui', 'Complete UI overview');
console.log('[FLOW 9] DONE');
});
});