515 lines
19 KiB
JavaScript
515 lines
19 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* Lars's Full User Testing Suite
|
|
* Testing Tilbudgivern from the perspective of a Danish master carpenter
|
|
* 47 years old, 22 years in business, specializes in roofing work (tagarbejde)
|
|
*/
|
|
|
|
const { test, expect } = require('@playwright/test');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const 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 });
|
|
}
|
|
|
|
async function takeScreenshot(page, name) {
|
|
const filePath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
|
await page.screenshot({ path: filePath, fullPage: true });
|
|
console.log(`Screenshot saved: ${filePath}`);
|
|
return filePath;
|
|
}
|
|
|
|
async function login(page) {
|
|
await page.goto(BASE_URL);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Try to find login form
|
|
const usernameField = page.locator('input[type="text"], input[name="username"], input[placeholder*="brugernavn" i], input[placeholder*="user" i]').first();
|
|
const passwordField = page.locator('input[type="password"]').first();
|
|
|
|
if (await usernameField.isVisible()) {
|
|
await usernameField.fill('toemrer');
|
|
await passwordField.fill('toemrer123');
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// TEST 1: LOGIN FLOW
|
|
// ============================================================
|
|
test('01 - Login flow', async ({ page }) => {
|
|
console.log('\n=== TEST 1: LOGIN FLOW ===');
|
|
|
|
await page.goto(BASE_URL);
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '01_login_page');
|
|
|
|
// Check what's on the login page
|
|
const pageTitle = await page.title();
|
|
const pageContent = await page.textContent('body');
|
|
console.log('Page title:', pageTitle);
|
|
console.log('Page has login form:', pageContent.includes('login') || pageContent.includes('Login') || pageContent.includes('brugernavn'));
|
|
|
|
// Find and fill login fields
|
|
const inputs = await page.locator('input').all();
|
|
console.log('Number of input fields:', inputs.length);
|
|
|
|
for (const input of inputs) {
|
|
const type = await input.getAttribute('type');
|
|
const placeholder = await input.getAttribute('placeholder');
|
|
const name = await input.getAttribute('name');
|
|
console.log(`Input: type=${type}, placeholder=${placeholder}, name=${name}`);
|
|
}
|
|
|
|
// Try login
|
|
const usernameInput = page.locator('input[type="text"], input:not([type="password"]):not([type="hidden"])').first();
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
|
|
if (await usernameInput.count() > 0) {
|
|
await usernameInput.fill('toemrer');
|
|
await passwordInput.fill('toemrer123');
|
|
await takeScreenshot(page, '01_login_filled');
|
|
|
|
// Find submit button
|
|
const submitBtn = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Log ind"), button:has-text("Sign in")').first();
|
|
if (await submitBtn.count() > 0) {
|
|
await submitBtn.click();
|
|
} else {
|
|
await page.keyboard.press('Enter');
|
|
}
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '01_after_login');
|
|
|
|
const currentUrl = page.url();
|
|
console.log('URL after login:', currentUrl);
|
|
console.log('Login successful:', !currentUrl.includes('login'));
|
|
}
|
|
|
|
// Report login page elements
|
|
const buttons = await page.locator('button').all();
|
|
for (const btn of buttons) {
|
|
const text = await btn.textContent();
|
|
console.log('Button:', text?.trim());
|
|
}
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 2: CREATE NEW PROJECT
|
|
// ============================================================
|
|
test('02 - Create new project', async ({ page }) => {
|
|
console.log('\n=== TEST 2: CREATE NEW PROJECT ===');
|
|
|
|
await login(page);
|
|
await takeScreenshot(page, '02_home_after_login');
|
|
|
|
// Log all visible text on the main page
|
|
const bodyText = await page.textContent('body');
|
|
console.log('Main page keywords found:');
|
|
const keywords = ['projekt', 'Projekt', 'nyt', 'Nyt', 'opret', 'Opret', 'tilbud', 'Tilbud', 'kunde', 'Kunde', 'new', 'New', 'create', 'Create'];
|
|
keywords.forEach(kw => {
|
|
if (bodyText.includes(kw)) console.log(` - "${kw}" found`);
|
|
});
|
|
|
|
// Find navigation items
|
|
const navItems = await page.locator('nav a, nav button, .nav a, .nav button, [role="navigation"] a, [role="navigation"] button').all();
|
|
console.log('Navigation items:');
|
|
for (const item of navItems) {
|
|
const text = await item.textContent();
|
|
const href = await item.getAttribute('href');
|
|
console.log(` - "${text?.trim()}" href=${href}`);
|
|
}
|
|
|
|
// Find "New project" button or similar
|
|
const createButtons = await page.locator('button, a').filter({ hasText: /ny|nyt|opret|create|new|projekt|tilbud/i }).all();
|
|
console.log('Create/New buttons found:', createButtons.length);
|
|
for (const btn of createButtons) {
|
|
const text = await btn.textContent();
|
|
console.log(` - "${text?.trim()}"`);
|
|
}
|
|
|
|
// Click the first "create" or "new" button
|
|
const newProjectBtn = page.locator('button, a').filter({ hasText: /nyt projekt|nyt tilbud|opret projekt|ny projekt|new project/i }).first();
|
|
if (await newProjectBtn.count() > 0) {
|
|
console.log('Found new project button, clicking...');
|
|
await newProjectBtn.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '02_new_project_form');
|
|
} else {
|
|
// Try clicking a "+" button or FAB
|
|
const fabBtn = page.locator('[aria-label*="add" i], [aria-label*="new" i], [aria-label*="create" i], button:has-text("+")').first();
|
|
if (await fabBtn.count() > 0) {
|
|
await fabBtn.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '02_after_fab_click');
|
|
}
|
|
}
|
|
|
|
// Check for project form fields
|
|
const formInputs = await page.locator('input, textarea').all();
|
|
console.log('Form inputs found:', formInputs.length);
|
|
for (const input of formInputs) {
|
|
const type = await input.getAttribute('type');
|
|
const placeholder = await input.getAttribute('placeholder');
|
|
const label = await input.getAttribute('aria-label');
|
|
const name = await input.getAttribute('name');
|
|
console.log(` Input: type=${type}, placeholder="${placeholder}", label="${label}", name="${name}"`);
|
|
}
|
|
|
|
// Fill in project details - try common field patterns
|
|
// Project name
|
|
const projectNameField = page.locator('input[name*="project" i], input[name*="projekt" i], input[placeholder*="projekt" i], input[placeholder*="navn" i]').first();
|
|
if (await projectNameField.count() > 0) {
|
|
await projectNameField.fill('Tagpap udskiftning - Møllevej 14');
|
|
console.log('Filled project name');
|
|
}
|
|
|
|
// Customer name
|
|
const customerNameField = page.locator('input[name*="customer" i], input[name*="kunde" i], input[placeholder*="kunde" i]').first();
|
|
if (await customerNameField.count() > 0) {
|
|
await customerNameField.fill('Jens Hansen');
|
|
console.log('Filled customer name');
|
|
}
|
|
|
|
// Email
|
|
const emailField = page.locator('input[type="email"], input[name*="email" i], input[placeholder*="email" i]').first();
|
|
if (await emailField.count() > 0) {
|
|
await emailField.fill('jens.hansen@gmail.com');
|
|
console.log('Filled email');
|
|
}
|
|
|
|
// Description
|
|
const descField = page.locator('textarea, input[name*="description" i], input[name*="beskrivelse" i]').first();
|
|
if (await descField.count() > 0) {
|
|
await descField.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.');
|
|
console.log('Filled description');
|
|
}
|
|
|
|
await takeScreenshot(page, '02_project_form_filled');
|
|
|
|
// Log all visible labels
|
|
const labels = await page.locator('label').all();
|
|
console.log('Form labels:');
|
|
for (const label of labels) {
|
|
const text = await label.textContent();
|
|
console.log(` - "${text?.trim()}"`);
|
|
}
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 3: GEOMETRY STEP
|
|
// ============================================================
|
|
test('03 - Geometry step', async ({ page }) => {
|
|
console.log('\n=== TEST 3: GEOMETRY STEP ===');
|
|
|
|
await login(page);
|
|
|
|
// Navigate to geometry - try finding it in the nav or creating a project first
|
|
// First try to navigate directly to a project with geometry
|
|
const projectLinks = await page.locator('a[href*="project"], a[href*="projekt"]').all();
|
|
if (projectLinks.length > 0) {
|
|
await projectLinks[0].click();
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
await takeScreenshot(page, '03_geometry_page_init');
|
|
|
|
// Look for geometry step indicators
|
|
const stepIndicators = await page.locator('[class*="step"], [class*="Step"], [class*="wizard"], [aria-label*="step"]').all();
|
|
console.log('Step indicators found:', stepIndicators.length);
|
|
|
|
// Look for geometry-related fields
|
|
const geometryKeywords = ['tagareal', 'areal', 'bredde', 'længde', 'hældning', 'grader', 'm2', 'tagtype'];
|
|
const bodyText = await page.textContent('body');
|
|
console.log('Geometry keywords found:');
|
|
geometryKeywords.forEach(kw => {
|
|
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
|
|
console.log(` - "${kw}" found`);
|
|
}
|
|
});
|
|
|
|
// Find geometry input fields
|
|
const numericInputs = await page.locator('input[type="number"]').all();
|
|
console.log('Numeric inputs:', numericInputs.length);
|
|
for (const input of numericInputs) {
|
|
const placeholder = await input.getAttribute('placeholder');
|
|
const label = await input.getAttribute('aria-label');
|
|
const name = await input.getAttribute('name');
|
|
const id = await input.getAttribute('id');
|
|
console.log(` Numeric input: placeholder="${placeholder}", label="${label}", name="${name}", id="${id}"`);
|
|
}
|
|
|
|
// Check for roof type selector
|
|
const roofTypeSelector = page.locator('select, [role="combobox"], [role="listbox"]').first();
|
|
if (await roofTypeSelector.count() > 0) {
|
|
console.log('Roof type selector found');
|
|
const options = await roofTypeSelector.locator('option').all();
|
|
for (const opt of options) {
|
|
const text = await opt.textContent();
|
|
console.log(` Roof type option: "${text?.trim()}"`);
|
|
}
|
|
}
|
|
|
|
await takeScreenshot(page, '03_geometry_fields');
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 4: SMART PACKAGES
|
|
// ============================================================
|
|
test('04 - Smart Package selection', async ({ page }) => {
|
|
console.log('\n=== TEST 4: SMART PACKAGE SELECTION ===');
|
|
|
|
await login(page);
|
|
await takeScreenshot(page, '04_home_for_packages');
|
|
|
|
// Look for smart packages in navigation
|
|
const smartPakkerLink = page.locator('a, button, [role="tab"]').filter({ hasText: /smart.?pakk|pakk|packages/i }).first();
|
|
if (await smartPakkerLink.count() > 0) {
|
|
console.log('Found Smart Pakker link, clicking...');
|
|
await smartPakkerLink.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '04_smart_pakker_page');
|
|
}
|
|
|
|
// Log page content related to packages
|
|
const bodyText = await page.textContent('body');
|
|
const packageKeywords = ['pakke', 'package', 'materiale', 'tagsten', 'tagpap', 'spær', 'tagrende'];
|
|
console.log('Package keywords found:');
|
|
packageKeywords.forEach(kw => {
|
|
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
|
|
console.log(` - "${kw}" found`);
|
|
}
|
|
});
|
|
|
|
// Find package cards/items
|
|
const packageCards = await page.locator('[class*="card"], [class*="Card"], [class*="pakke"], [class*="package"]').all();
|
|
console.log('Package cards found:', packageCards.length);
|
|
|
|
// List package names
|
|
const packageTitles = await page.locator('h2, h3, h4, [class*="title"], [class*="name"]').all();
|
|
console.log('Package titles found:');
|
|
for (const title of packageTitles) {
|
|
const text = await title.textContent();
|
|
if (text && text.trim().length > 0 && text.trim().length < 100) {
|
|
console.log(` - "${text.trim()}"`);
|
|
}
|
|
}
|
|
|
|
await takeScreenshot(page, '04_packages_listed');
|
|
|
|
// Try to select a package
|
|
const selectButtons = await page.locator('button').filter({ hasText: /vælg|select|tilføj|add/i }).all();
|
|
console.log('Select/Add buttons:', selectButtons.length);
|
|
if (selectButtons.length > 0) {
|
|
const firstBtnText = await selectButtons[0].textContent();
|
|
console.log('Clicking first select button:', firstBtnText);
|
|
await selectButtons[0].click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '04_package_selected');
|
|
}
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 5: FINAL REVIEW
|
|
// ============================================================
|
|
test('05 - Final Review', async ({ page }) => {
|
|
console.log('\n=== TEST 5: FINAL REVIEW ===');
|
|
|
|
await login(page);
|
|
|
|
// Look for review/quote links
|
|
const reviewLink = page.locator('a, button').filter({ hasText: /gennemse|review|tilbud|quote|opsummering|summary/i }).first();
|
|
if (await reviewLink.count() > 0) {
|
|
await reviewLink.click();
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
await takeScreenshot(page, '05_final_review');
|
|
|
|
// Check for pricing elements
|
|
const bodyText = await page.textContent('body');
|
|
const priceKeywords = ['kr', 'pris', 'total', 'subtotal', 'moms', 'mva', 'inkl', 'eksl'];
|
|
console.log('Price-related content:');
|
|
priceKeywords.forEach(kw => {
|
|
if (bodyText.toLowerCase().includes(kw.toLowerCase())) {
|
|
console.log(` - "${kw}" found`);
|
|
}
|
|
});
|
|
|
|
// Look for line items / materials table
|
|
const tables = await page.locator('table, [class*="table"], [class*="Table"]').all();
|
|
console.log('Tables found:', tables.length);
|
|
|
|
// Check for PDF/send buttons
|
|
const actionButtons = await page.locator('button').all();
|
|
console.log('Action buttons on review page:');
|
|
for (const btn of actionButtons) {
|
|
const text = await btn.textContent();
|
|
if (text && text.trim().length > 0) {
|
|
console.log(` - "${text.trim()}"`);
|
|
}
|
|
}
|
|
|
|
await takeScreenshot(page, '05_review_content');
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 6: EXPLORE THE FULL APP (comprehensive navigation)
|
|
// ============================================================
|
|
test('06 - Full app exploration', async ({ page }) => {
|
|
console.log('\n=== TEST 6: FULL APP EXPLORATION ===');
|
|
|
|
await login(page);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
const currentUrl = page.url();
|
|
console.log('Landing URL after login:', currentUrl);
|
|
|
|
await takeScreenshot(page, '06_landing_page');
|
|
|
|
// Get full page text for analysis
|
|
const bodyText = await page.textContent('body');
|
|
console.log('\n--- PAGE TEXT (first 2000 chars) ---');
|
|
console.log(bodyText.substring(0, 2000));
|
|
|
|
// Find all navigation links
|
|
const allLinks = await page.locator('a[href]').all();
|
|
console.log('\n--- ALL LINKS ---');
|
|
for (const link of allLinks) {
|
|
const text = await link.textContent();
|
|
const href = await link.getAttribute('href');
|
|
if (text && text.trim() && href) {
|
|
console.log(` "${text.trim()}" -> ${href}`);
|
|
}
|
|
}
|
|
|
|
// Find all buttons
|
|
const allButtons = await page.locator('button:visible').all();
|
|
console.log('\n--- ALL VISIBLE BUTTONS ---');
|
|
for (const btn of allButtons) {
|
|
const text = await btn.textContent();
|
|
if (text && text.trim()) {
|
|
console.log(` "${text.trim()}"`);
|
|
}
|
|
}
|
|
|
|
// Check for sidebar/menu
|
|
const sidebar = page.locator('[class*="sidebar"], [class*="Sidebar"], [class*="drawer"], [role="navigation"]').first();
|
|
if (await sidebar.count() > 0) {
|
|
console.log('\nSidebar found');
|
|
const sidebarText = await sidebar.textContent();
|
|
console.log('Sidebar text:', sidebarText?.substring(0, 500));
|
|
}
|
|
|
|
// Check MUI tabs/stepper
|
|
const tabs = await page.locator('[role="tab"]').all();
|
|
console.log('\n--- TABS ---');
|
|
for (const tab of tabs) {
|
|
const text = await tab.textContent();
|
|
console.log(` Tab: "${text?.trim()}"`);
|
|
}
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 7: NAVIGATE TO PLANNING DASHBOARD
|
|
// ============================================================
|
|
test('07 - Planning Dashboard', async ({ page }) => {
|
|
console.log('\n=== TEST 7: PLANNING DASHBOARD ===');
|
|
|
|
await login(page);
|
|
|
|
// Try to find planning/planlægning link
|
|
const planningLink = page.locator('a, button, [role="tab"]').filter({ hasText: /planlæ|planning|kalender|calendar|schedule/i }).first();
|
|
if (await planningLink.count() > 0) {
|
|
console.log('Found planning link, clicking...');
|
|
await planningLink.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '07_planning_dashboard');
|
|
} else {
|
|
// Try navigating directly
|
|
await page.goto(`${BASE_URL}/planning`);
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '07_planning_direct');
|
|
|
|
if (page.url().includes('login')) {
|
|
await login(page);
|
|
await page.goto(`${BASE_URL}/planning`);
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
}
|
|
|
|
const bodyText = await page.textContent('body');
|
|
console.log('Planning page text (first 1000 chars):', bodyText.substring(0, 1000));
|
|
|
|
// Check for calendar elements
|
|
const calendarElements = await page.locator('[class*="calendar"], [class*="Calendar"], [class*="cal"]').all();
|
|
console.log('Calendar elements found:', calendarElements.length);
|
|
|
|
await takeScreenshot(page, '07_planning_content');
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|
|
|
|
// ============================================================
|
|
// TEST 8: SMART PAKKER MANAGEMENT
|
|
// ============================================================
|
|
test('08 - Smart Pakker management', async ({ page }) => {
|
|
console.log('\n=== TEST 8: SMART PAKKER MANAGEMENT ===');
|
|
|
|
await login(page);
|
|
|
|
// Try to find settings or admin area
|
|
const settingsLink = page.locator('a, button').filter({ hasText: /indstillinger|settings|admin|konfiguration/i }).first();
|
|
if (await settingsLink.count() > 0) {
|
|
await settingsLink.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '08_settings_page');
|
|
}
|
|
|
|
// Try direct navigation to smart packages admin
|
|
const paths = ['/smart-pakker', '/admin/smart-pakker', '/settings', '/admin'];
|
|
for (const urlPath of paths) {
|
|
await page.goto(`${BASE_URL}${urlPath}`);
|
|
await page.waitForLoadState('networkidle');
|
|
const url = page.url();
|
|
const bodyText = await page.textContent('body');
|
|
if (!url.includes('login') && (bodyText.includes('pakk') || bodyText.includes('package'))) {
|
|
console.log(`Found smart packages at: ${urlPath}`);
|
|
await takeScreenshot(page, `08_smart_pakker_${urlPath.replace(/\//g, '_')}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const bodyText = await page.textContent('body');
|
|
console.log('Page content (first 1000 chars):', bodyText.substring(0, 1000));
|
|
|
|
// Look for "create new package" button
|
|
const createPkgBtn = page.locator('button').filter({ hasText: /ny pakke|opret pakke|new package|create package|tilføj pakke/i }).first();
|
|
if (await createPkgBtn.count() > 0) {
|
|
console.log('Found create package button');
|
|
await createPkgBtn.click();
|
|
await page.waitForLoadState('networkidle');
|
|
await takeScreenshot(page, '08_create_package_form');
|
|
}
|
|
|
|
expect(page.url()).toBeDefined();
|
|
});
|