Files
tilbudgivern/tests/carpenter-quote-creation.spec.js

387 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { test, expect } = require('@playwright/test');
/**
* Carpenter Quote Creation Test
*
* This test creates a complete carpenter quote through the ProjectFlow:
* 1. Create a customer project
* 2. Enter geometry data
* 3. Select a smart package
* 4. Generate and review the quote
*
* The goal is to verify the quote is realistic and complete.
*/
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.describe('🔨 Carpenter Quote Creation - Full User Journey', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the application
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
// Login if needed
const loginButton = page.locator('button:has-text("Log ind")');
if (await loginButton.isVisible({ timeout: 2000 }).catch(() => false)) {
console.log('🔐 Logging in...');
await page.fill('input[type="text"], input[name="username"]', 'admin');
await page.fill('input[type="password"], input[name="password"]', 'admin123');
await loginButton.click();
await page.waitForTimeout(2000);
}
});
test('Create complete carpenter quote for terrace project', async ({ page }) => {
console.log('\n🎬 Starting carpenter quote creation test...\n');
// Step 1: Navigate to Project Flow
console.log('📋 Step 1: Navigating to Project Flow...');
const projectFlowButton = page.locator('button:has-text("Projekt Flow"), .nav-btn:has-text("Projekt Flow")');
if (await projectFlowButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await projectFlowButton.click();
await page.waitForTimeout(1500);
console.log('✅ Navigated to Project Flow');
} else {
console.log('⚠️ Already in Project Flow or button not found');
}
// Take initial screenshot
await page.screenshot({ path: 'test-results/01-project-flow-start.png', fullPage: true });
// Step 2: Create a new project
console.log('\n🏗 Step 2: Creating new customer project...');
// Look for "Opret Nyt Projekt" or similar button
const createProjectButton = page.locator(
'button:has-text("Opret"), button:has-text("Nyt Projekt"), button:has-text("Ny Kunde")'
).first();
if (await createProjectButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await createProjectButton.click();
await page.waitForTimeout(1000);
}
// Fill in customer information
const customerName = 'Test Kunde - ' + new Date().toISOString().split('T')[0];
console.log(` Customer: ${customerName}`);
// Try to find and fill customer name field
const nameFields = [
'input[name="customerName"]',
'input[placeholder*="navn" i]',
'input[placeholder*="kunde" i]',
'input[id*="name" i]'
];
for (const selector of nameFields) {
const field = page.locator(selector).first();
if (await field.isVisible({ timeout: 1000 }).catch(() => false)) {
await field.fill(customerName);
console.log(' ✓ Filled customer name');
break;
}
}
// Fill address
const addressField = page.locator('input[name*="address"], input[placeholder*="adresse" i]').first();
if (await addressField.isVisible({ timeout: 1000 }).catch(() => false)) {
await addressField.fill('Testvej 123, 4600 Køge');
console.log(' ✓ Filled address');
}
// Fill phone
const phoneField = page.locator('input[name*="phone"], input[placeholder*="telefon" i], input[type="tel"]').first();
if (await phoneField.isVisible({ timeout: 1000 }).catch(() => false)) {
await phoneField.fill('42 46 81 10');
console.log(' ✓ Filled phone');
}
// Fill email
const emailField = page.locator('input[name*="email"], input[type="email"]').first();
if (await emailField.isVisible({ timeout: 1000 }).catch(() => false)) {
await emailField.fill('test@example.dk');
console.log(' ✓ Filled email');
}
// Fill project description
const descriptionField = page.locator('textarea, input[name*="description"], input[name*="project"]').first();
if (await descriptionField.isVisible({ timeout: 1000 }).catch(() => false)) {
await descriptionField.fill('Terrasse på 25 m² med douglasgran og skruer');
console.log(' ✓ Filled project description');
}
await page.screenshot({ path: 'test-results/02-customer-info-filled.png', fullPage: true });
// Save/Create project
const saveProjectButton = page.locator(
'button:has-text("Gem"), button:has-text("Opret"), button:has-text("Fortsæt"), button:has-text("Næste")'
).first();
if (await saveProjectButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await saveProjectButton.click();
await page.waitForTimeout(2000);
console.log('✅ Project created');
}
// Step 3: Enter Geometry Data
console.log('\n📐 Step 3: Entering geometry data...');
await page.waitForTimeout(1000);
// Look for geometry inputs (area, dimensions, etc.)
const areaInput = page.locator('input[name*="area"], input[placeholder*="areal" i], input[id*="area" i]').first();
if (await areaInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await areaInput.fill('25');
console.log(' ✓ Entered area: 25 m²');
}
// Look for length/width inputs
const lengthInput = page.locator('input[name*="length"], input[placeholder*="længde" i]').first();
if (await lengthInput.isVisible({ timeout: 1000 }).catch(() => false)) {
await lengthInput.fill('5');
console.log(' ✓ Entered length: 5 m');
}
const widthInput = page.locator('input[name*="width"], input[placeholder*="bredde" i]').first();
if (await widthInput.isVisible({ timeout: 1000 }).catch(() => false)) {
await widthInput.fill('5');
console.log(' ✓ Entered width: 5 m');
}
await page.screenshot({ path: 'test-results/03-geometry-filled.png', fullPage: true });
// Continue to next step
const nextButton = page.locator('button:has-text("Næste"), button:has-text("Fortsæt"), button:has-text("Gem og fortsæt")').first();
if (await nextButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await nextButton.click();
await page.waitForTimeout(2000);
console.log('✅ Geometry saved, moving to next step');
}
// Step 4: Select Smart Package
console.log('\n🔧 Step 4: Selecting Smart Package...');
await page.waitForTimeout(1500);
// Look for package cards or selection buttons
const packageCards = page.locator('.package-card, .smart-package, [class*="package"]');
const packageCount = await packageCards.count();
console.log(` Found ${packageCount} package options`);
if (packageCount > 0) {
// Click the first suitable package (or look for terrace-related)
const terracePackage = page.locator(':text("Terrasse"), :text("terrasse")').first();
if (await terracePackage.isVisible({ timeout: 2000 }).catch(() => false)) {
// Find the select button near the terrace text
const selectButton = page.locator('button:near(:text("Terrasse"))').first();
if (await selectButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await selectButton.click();
console.log(' ✓ Selected Terrace package');
}
} else {
// Just click the first package
const firstPackageButton = packageCards.first().locator('button').first();
if (await firstPackageButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await firstPackageButton.click();
console.log(' ✓ Selected first available package');
}
}
await page.waitForTimeout(1500);
}
await page.screenshot({ path: 'test-results/04-package-selected.png', fullPage: true });
// Step 5: Review materials and labor
console.log('\n📊 Step 5: Reviewing materials and labor...');
// Wait for calculations to complete
await page.waitForTimeout(2000);
// Look for material list
const materialsSection = page.locator(':text("Materialer"), .materials, [class*="material"]');
if (await materialsSection.isVisible({ timeout: 2000 }).catch(() => false)) {
console.log(' ✓ Materials section visible');
// Try to extract material information
const pageText = await page.textContent('body');
const materialMatches = pageText.match(/(\d+[\.,]?\d*)\s*(m²|m|stk|kg|liter|pakke)/gi);
if (materialMatches) {
console.log(' 📦 Materials found:', materialMatches.slice(0, 5).join(', '));
}
}
// Look for labor/work hours
const laborSection = page.locator(':text("Timer"), :text("Arbejdstid"), :text("tømrer")');
if (await laborSection.isVisible({ timeout: 2000 }).catch(() => false)) {
console.log(' ✓ Labor section visible');
}
await page.screenshot({ path: 'test-results/05-materials-labor.png', fullPage: true });
// Continue to Final Review
const continueButton = page.locator('button:has-text("Næste"), button:has-text("Fortsæt"), button:has-text("Gennemgang")').first();
if (await continueButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await continueButton.click();
await page.waitForTimeout(2000);
console.log('✅ Moving to Final Review');
}
// Step 6: Final Review and Quote Generation
console.log('\n🎯 Step 6: Final Review and Quote Generation...');
await page.waitForTimeout(1500);
// Look for total price
const priceElements = page.locator(':text("Total"), :text("Pris"), :text("kr")');
const priceCount = await priceElements.count();
if (priceCount > 0) {
const pageText = await page.textContent('body');
// Extract prices (look for DKK amounts)
const priceMatches = pageText.match(/(\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?)\s*kr/gi);
if (priceMatches) {
console.log(' 💰 Prices found:', priceMatches.slice(0, 5).join(', '));
// Extract the largest price (likely the total)
const prices = priceMatches.map(p => {
const num = p.replace(/[^\d.,]/g, '').replace(/\./g, '').replace(',', '.');
return parseFloat(num);
}).filter(p => !isNaN(p));
if (prices.length > 0) {
const total = Math.max(...prices);
console.log(` 🎯 Estimated total: ${total.toLocaleString('da-DK')} kr`);
}
}
}
await page.screenshot({ path: 'test-results/06-final-review.png', fullPage: true });
// Look for quote text/description
const quoteTextArea = page.locator('textarea[class*="quote"], textarea:near(:text("Tilbud"))').first();
if (await quoteTextArea.isVisible({ timeout: 2000 }).catch(() => false)) {
const quoteText = await quoteTextArea.inputValue();
console.log('\n📄 Quote Text Preview:');
console.log('─'.repeat(60));
console.log(quoteText.substring(0, 500));
console.log('─'.repeat(60));
}
// Try to generate static quote (without sending to ordrestyring)
const staticQuoteButton = page.locator(
'button:has-text("Statisk"), button:has-text("Gem tilbud"), button:has-text("Generer")'
).first();
if (await staticQuoteButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await staticQuoteButton.click();
await page.waitForTimeout(3000);
console.log('✅ Quote generation requested');
}
await page.screenshot({ path: 'test-results/07-quote-generated.png', fullPage: true });
// Extract complete quote data from page
console.log('\n📊 Extracting complete quote data...');
const fullPageText = await page.textContent('body');
// Save complete quote data to file
const fs = require('fs');
const quoteData = {
timestamp: new Date().toISOString(),
customerName: customerName,
project: 'Terrasse 25 m²',
pageContent: fullPageText,
screenshots: [
'01-project-flow-start.png',
'02-customer-info-filled.png',
'03-geometry-filled.png',
'04-package-selected.png',
'05-materials-labor.png',
'06-final-review.png',
'07-quote-generated.png'
]
};
fs.writeFileSync(
'test-results/carpenter-quote-data.json',
JSON.stringify(quoteData, null, 2)
);
console.log('✅ Quote data saved to test-results/carpenter-quote-data.json');
// Final summary
console.log('\n' + '='.repeat(60));
console.log('✅ CARPENTER QUOTE CREATION TEST COMPLETED');
console.log('='.repeat(60));
console.log(`Customer: ${customerName}`);
console.log('Project: Terrasse 25 m² med douglasgran');
console.log('Screenshots: 7 saved to test-results/');
console.log('Data file: carpenter-quote-data.json');
console.log('='.repeat(60) + '\n');
// Basic assertion to ensure test passes
expect(fullPageText.length).toBeGreaterThan(100);
});
test('Verify quote contains realistic carpenter data', async ({ page }) => {
console.log('\n🔍 Verifying quote realism...\n');
// This test can run after the first test or independently
// It checks if the generated quote contains realistic elements
const fs = require('fs');
let quoteData;
try {
const dataFile = fs.readFileSync('test-results/carpenter-quote-data.json', 'utf8');
quoteData = JSON.parse(dataFile);
} catch (error) {
console.log('⚠️ No quote data found, skipping verification');
return;
}
const content = quoteData.pageContent.toLowerCase();
console.log('Checking for realistic carpenter quote elements...\n');
const checks = [
{ name: 'Customer name', test: () => content.includes(quoteData.customerName.toLowerCase()) },
{ name: 'Materials (wood/timber)', test: () => content.includes('træ') || content.includes('tømmer') || content.includes('douglasgran') },
{ name: 'Area (m²)', test: () => content.includes('m²') || content.includes('areal') },
{ name: 'Price in DKK', test: () => content.includes('kr') || content.includes('dkk') },
{ name: 'Labor/hours', test: () => content.includes('timer') || content.includes('arbejdstid') || content.includes('tømrer') },
{ name: 'VAT/Moms', test: () => content.includes('moms') || content.includes('25%') },
{ name: 'Company name', test: () => content.includes('holck') || content.includes('tømrer') },
{ name: 'Contact info', test: () => content.includes('telefon') || content.includes('email') || content.includes('42 46') }
];
let passed = 0;
let failed = 0;
checks.forEach(check => {
const result = check.test();
if (result) {
console.log(`${check.name}`);
passed++;
} else {
console.log(`${check.name}`);
failed++;
}
});
console.log('\n' + '='.repeat(60));
console.log(`REALISM CHECK: ${passed}/${checks.length} passed`);
console.log('='.repeat(60) + '\n');
// Test should pass if at least 60% of checks pass
expect(passed).toBeGreaterThanOrEqual(Math.floor(checks.length * 0.6));
});
});