231 lines
10 KiB
JavaScript
231 lines
10 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
|
|
const views = ['projects', 'planning', 'mobile', 'materials', 'smart-packages', 'openai-usage'];
|
|
|
|
async function openAuthenticatedView(page, view, viewport) {
|
|
await page.setViewportSize(viewport);
|
|
await page.addInitScript((selectedView) => {
|
|
window.localStorage.setItem('accessToken', 'ui-layout-test-token');
|
|
window.localStorage.setItem('user', JSON.stringify({ username: 'ui-layout-test' }));
|
|
window.localStorage.setItem('tilbudgivern_current_view', selectedView);
|
|
}, view);
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await expect(page.locator('.App')).toBeVisible();
|
|
await page.waitForTimeout(view === 'planning' ? 2500 : 750);
|
|
}
|
|
|
|
async function expectLayoutInsideViewport(page, viewport) {
|
|
const geometry = await page.evaluate(() => {
|
|
const pageWidth = document.documentElement.scrollWidth;
|
|
const viewportWidth = window.innerWidth;
|
|
const header = document.querySelector('.header')?.getBoundingClientRect();
|
|
const statusElement = document.querySelector('.system-status-button');
|
|
const status = statusElement?.getBoundingClientRect();
|
|
|
|
const overlapCandidates = Array.from(document.querySelectorAll(
|
|
'.main-content .card, .main-content .panel, .main-content section, .usage-panel, .stat-card'
|
|
)).filter((element) => {
|
|
const style = window.getComputedStyle(element);
|
|
const rect = element.getBoundingClientRect();
|
|
return style.display !== 'none' && style.visibility !== 'hidden' &&
|
|
style.position !== 'absolute' && style.position !== 'fixed' &&
|
|
rect.width > 20 && rect.height > 20;
|
|
});
|
|
|
|
const overlaps = [];
|
|
for (let index = 0; index < overlapCandidates.length; index += 1) {
|
|
for (let otherIndex = index + 1; otherIndex < overlapCandidates.length; otherIndex += 1) {
|
|
const first = overlapCandidates[index];
|
|
const second = overlapCandidates[otherIndex];
|
|
if (first.parentElement !== second.parentElement) continue;
|
|
const a = first.getBoundingClientRect();
|
|
const b = second.getBoundingClientRect();
|
|
const overlapX = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
|
const overlapY = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
|
if (overlapX > 2 && overlapY > 2) {
|
|
overlaps.push(`${first.className || first.tagName} <> ${second.className || second.tagName}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
pageWidth,
|
|
viewportWidth,
|
|
statusInsideHeader: Boolean(statusElement?.closest('.header') && header && status &&
|
|
status.left >= header.left && status.right <= header.right &&
|
|
status.top >= header.top && status.bottom <= header.bottom),
|
|
overlaps
|
|
};
|
|
});
|
|
|
|
expect(geometry.pageWidth, `page width at ${viewport.width}px`).toBeLessThanOrEqual(geometry.viewportWidth);
|
|
expect(geometry.statusInsideHeader).toBe(true);
|
|
expect(geometry.overlaps).toEqual([]);
|
|
}
|
|
|
|
test.describe('professional responsive shell', () => {
|
|
test('login remains polished and contained at 360px', async ({ page }) => {
|
|
await page.setViewportSize({ width: 360, height: 800 });
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await expect(page.getByRole('heading', { name: 'Tilbudgivern' })).toBeVisible();
|
|
await expect(page.getByLabel('Brugernavn')).toBeVisible();
|
|
await expect(page.getByLabel('Password')).toBeVisible();
|
|
const form = await page.locator('.login-form').boundingBox();
|
|
expect(form).not.toBeNull();
|
|
expect(form.x).toBeGreaterThanOrEqual(0);
|
|
expect(form.x + form.width).toBeLessThanOrEqual(360);
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(360);
|
|
});
|
|
|
|
for (const viewport of [
|
|
{ name: 'desktop', width: 1440, height: 1000 },
|
|
{ name: 'mobile', width: 360, height: 800 }
|
|
]) {
|
|
test(`${viewport.name} keeps every primary view inside the viewport`, async ({ page }) => {
|
|
for (const view of views) {
|
|
await openAuthenticatedView(page, view, viewport);
|
|
await expectLayoutInsideViewport(page, viewport);
|
|
|
|
if (view === 'smart-packages') {
|
|
await expect(page.getByRole('button', { name: 'Ny smart pakke', exact: true })).toBeVisible();
|
|
await expect(page.locator('.MuiFab-root')).toHaveCount(0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test('mobile navigation and support dialog remain usable at 360px', async ({ page }) => {
|
|
const viewport = { width: 360, height: 800 };
|
|
await openAuthenticatedView(page, 'projects', viewport);
|
|
|
|
const menuButton = page.getByRole('button', { name: 'Menu' });
|
|
await expect(menuButton).toHaveAttribute('aria-expanded', 'false');
|
|
await menuButton.click();
|
|
await expect(menuButton).toHaveAttribute('aria-expanded', 'true');
|
|
await expect(page.getByRole('navigation', { name: 'Hovednavigation' })).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: 'Åbn support' }).click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible();
|
|
const box = await dialog.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box.x).toBeGreaterThanOrEqual(0);
|
|
expect(box.y).toBeGreaterThanOrEqual(0);
|
|
expect(box.x + box.width).toBeLessThanOrEqual(viewport.width);
|
|
expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
|
|
});
|
|
|
|
test('AI usage table becomes labelled cards on mobile', async ({ page }) => {
|
|
await openAuthenticatedView(page, 'openai-usage', { width: 360, height: 800 });
|
|
const firstCell = page.locator('.usage-category-table td[data-label]').first();
|
|
await expect(firstCell).toBeVisible();
|
|
await expect(firstCell).toHaveCSS('display', 'grid');
|
|
await expect(firstCell).not.toHaveAttribute('data-label', '');
|
|
});
|
|
|
|
test('Smart Packages keeps desktop columns separate and shows every mobile import step', async ({ page }) => {
|
|
await openAuthenticatedView(page, 'smart-packages', { width: 1440, height: 1000 });
|
|
const geometry = await page.evaluate(() => {
|
|
const title = Array.from(document.querySelectorAll('h1, h2'))
|
|
.find((element) => element.textContent.trim() === 'Smart Pakker')
|
|
?.getBoundingClientRect();
|
|
const sidebar = Array.from(document.querySelectorAll('.MuiPaper-root'))
|
|
.find((element) => element.innerText.includes('Smart Pakker &'))
|
|
?.getBoundingClientRect();
|
|
return {
|
|
titleLeft: title?.left,
|
|
sidebarRight: sidebar?.right
|
|
};
|
|
});
|
|
expect(geometry.titleLeft).toBeGreaterThanOrEqual(geometry.sidebarRight);
|
|
|
|
await openAuthenticatedView(page, 'smart-packages', { width: 360, height: 800 });
|
|
const stepLabels = page.locator('.MuiStepLabel-label');
|
|
await expect(stepLabels).toHaveCount(6);
|
|
for (let index = 0; index < 6; index += 1) {
|
|
await expect(stepLabels.nth(index)).toBeVisible();
|
|
}
|
|
await expect(page.locator('.MuiStepper-root')).toHaveCSS('flex-direction', 'column');
|
|
});
|
|
|
|
test('planning loading copy stays horizontal while only the spinner rotates', async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem('accessToken', 'ui-layout-test-token');
|
|
window.localStorage.setItem('user', JSON.stringify({ username: 'ui-layout-test' }));
|
|
window.localStorage.setItem('tilbudgivern_current_view', 'planning');
|
|
});
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
const loadingCopy = page.locator('.planning-loading-content');
|
|
await expect(loadingCopy).toBeVisible();
|
|
await expect(loadingCopy).toHaveCSS('animation-name', 'none');
|
|
await expect(loadingCopy).toHaveCSS('transform', 'none');
|
|
await expect(loadingCopy.locator('.spinner')).not.toHaveCSS('animation-name', 'none');
|
|
});
|
|
|
|
test('mobile filters are labelled and the order list has a bounded height', async ({ page }) => {
|
|
await openAuthenticatedView(page, 'mobile', { width: 360, height: 800 });
|
|
await expect(page.getByLabel('Filtrer efter ordretype')).toBeVisible();
|
|
const orderList = page.locator('.mobile-order-list');
|
|
await expect(orderList).toHaveCSS('max-height', '320px');
|
|
await expect(orderList).toHaveCSS('overflow-y', 'auto');
|
|
|
|
await openAuthenticatedView(page, 'materials', { width: 360, height: 800 });
|
|
await expect(page.getByLabel('Antal materialer per side')).toBeAttached();
|
|
});
|
|
|
|
test('continue in Project Flow renders Smart Package without a white screen', async ({ page }) => {
|
|
const pageErrors = [];
|
|
page.on('pageerror', (error) => pageErrors.push(error.message));
|
|
await page.route('**/api/customer-projects/**', async (route) => {
|
|
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(route.request().method())) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ success: true })
|
|
});
|
|
return;
|
|
}
|
|
await route.continue();
|
|
});
|
|
|
|
await openAuthenticatedView(page, 'projects', { width: 1440, height: 1000 });
|
|
await page.getByRole('button', { name: 'Fortsæt', exact: true }).click();
|
|
await expect(page.getByRole('heading', { name: /Fortsæt til Smart Pakke/i })).toBeVisible();
|
|
await expect(page.locator('body')).not.toBeEmpty();
|
|
expect(pageErrors).toEqual([]);
|
|
});
|
|
|
|
test('customer search accepts a street address and shows the full address', async ({ page }) => {
|
|
page.on('dialog', (dialog) => dialog.dismiss());
|
|
await page.route('**/api/customers/search**', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
success: true,
|
|
count: 1,
|
|
customers: [{
|
|
customerNumber: '2003',
|
|
name: 'Alice',
|
|
address: 'Hornumvej 10',
|
|
postalCode: '4600',
|
|
city: 'Køge'
|
|
}]
|
|
})
|
|
});
|
|
});
|
|
|
|
await openAuthenticatedView(page, 'projects', { width: 1440, height: 1000 });
|
|
await page.evaluate(() => {
|
|
Array.from(document.querySelectorAll('button'))
|
|
.find((button) => button.textContent.includes('Nyt Projekt'))
|
|
?.click();
|
|
});
|
|
const customerSearch = page.getByLabel('Søg efter kunde på navn, kundenummer eller adresse');
|
|
await expect(customerSearch).toBeVisible();
|
|
await customerSearch.fill('Hornumvej 10');
|
|
await expect(page.getByText('📍 Hornumvej 10, 4600 Køge')).toBeVisible();
|
|
});
|
|
});
|