Files
delivery-app-web/tests/e2e/delivery.spec.ts
T

267 lines
18 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
import { testAccount } from '../helpers/account';
import { randomUUID } from 'node:crypto';
test.beforeEach(async ({ context }) => {
const request = context.request;
const state = await (await request.get('/api/local-admin/status')).json();
const result = await request.post(`/api/local-admin/${state.setupRequired ? 'setup' : 'login'}`, { headers: { 'x-model-request': '1' }, data: state.setupRequired ? { ...testAccount, setupToken: 'isolated-test-bootstrap' } : { username: testAccount.username, password: testAccount.password } });
expect(result.ok()).toBe(true);
});
async function deliveryForm(page: Page, notes: string) {
await page.getByRole('combobox', { name: 'לקוח', exact: true }).selectOption({ label: 'מרפאת הדגמה' });
await page.getByRole('combobox', { name: 'כתובת למשלוח', exact: true }).selectOption('20000000-0000-4000-8000-000000000002');
await page.getByRole('combobox', { name: 'סוג משלוח', exact: true }).selectOption('10000000-0000-4000-8000-000000000003');
await page.getByLabel('מחיר ליחידה (₪)').fill('10.01');
await page.getByRole('textbox', { name: /הערות למשלוח/ }).fill(notes);
}
async function openDeliveryView(page: Page) {
const heading = page.getByRole('heading', { name: 'המשלוחים שלך' });
if (!await heading.isVisible()) await page.getByRole('button', { name: 'משלוחים', exact: true }).click();
await expect(heading).toBeVisible();
}
test('online creation then offline save, offline reload, reconnect and server verification', async ({ page, context }) => {
const request = context.request;
const browserErrors: string[] = [];
page.on('pageerror', error => browserErrors.push(error.message));
await page.goto('/');
await expect(page.getByRole('heading', { name: 'תמונת המצב שלך' })).toBeVisible();
await expect(page.locator('.day-card')).toHaveCount(7);
await expect(page.getByRole('region', { name: 'פעולות מהירות' }).getByRole('button')).toHaveCount(4);
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect.poll(() => page.locator('.sidebar').evaluate(element => { const rect = element.getBoundingClientRect(); return rect.left >= innerWidth || rect.right <= 0; })).toBe(true);
await page.screenshot({ path: 'test-results/model-delivery-dashboard-mobile.png' });
await page.setViewportSize({ width: 1440, height: 900 });
await page.getByRole('button', { name: /משלוח חדש/ }).click();
await expect(page.getByRole('heading', { name: 'המשלוחים שלך' })).toBeVisible();
await expect(page.getByRole('combobox', { name: 'לקוח', exact: true }).locator('option')).toContainText(['בחירת לקוח', 'מרפאת הדגמה']);
await expect(page.getByLabel('תאריך מתוכנן')).toHaveValue(/^\d{4}-\d{2}-\d{2}$/);
await page.evaluate(async () => { await navigator.serviceWorker.ready; });
await page.reload();
await openDeliveryView(page);
await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true);
const onlineNotes = `Online browser test ${Date.now()}`;
await deliveryForm(page, onlineNotes);
await page.getByRole('radio', { name: /שעה אחרת/ }).check();
await page.getByLabel('שעה מדויקת', { exact: true }).fill('14:35');
await page.getByRole('button', { name: '+ שמירת משלוח', exact: true }).click();
await expect(page.locator('.detail-panel')).toContainText(onlineNotes);
await expect(page.locator('.detail-panel')).toContainText('שעה אחרת · 14:35');
await expect(page.locator('.delivery-row[aria-pressed="true"]')).toContainText('מסונכרן');
await expect(page.locator('.delivery-row[aria-pressed="true"]')).toContainText('#');
await context.setOffline(true);
await expect(page.locator('.connection')).toContainText('ללא חיבור לרשת');
const offlineNotes = `Offline browser test ${Date.now()}`;
await deliveryForm(page, offlineNotes);
await page.getByRole('radio', { name: /צהריים/ }).check();
await page.getByRole('button', { name: '+ שמירת משלוח', exact: true }).click();
await expect(page.locator('.detail-panel')).toContainText(offlineNotes);
const offlineRow = page.locator('.delivery-row[aria-pressed="true"]');
await expect(offlineRow).toContainText('ממתין לסנכרון');
await expect(offlineRow).toContainText('ממתין למספר');
const rowId = await offlineRow.getAttribute('data-testid');
expect(rowId).toBeTruthy();
await page.reload();
await openDeliveryView(page);
await expect(page.getByTestId(rowId!)).toContainText('ממתין לסנכרון');
await expect(page.getByTestId(rowId!)).toContainText('ממתין למספר');
await context.setOffline(false);
try { await expect(page.getByTestId(rowId!)).toContainText('מסונכרן'); }
catch (error) {
console.log(await page.evaluate(async () => ({ online: navigator.onLine, connection: document.querySelector('.connection')?.textContent, sync: document.querySelector('.sync-button')?.textContent, alerts: Array.from(document.querySelectorAll('.alert')).map(e => e.textContent), locks: await navigator.locks.query() })));
throw error;
}
await expect(page.getByTestId(rowId!)).toContainText('#');
await page.reload();
await openDeliveryView(page);
await expect(page.getByTestId(rowId!)).toContainText('מסונכרן');
const allChanges: Array<{ change: { entityType: string; record: { id: string; notes?: string; deliveryNumber?: number; deliverySlot?: string; specificTime?: string | null } } }> = [];
let cursor = '0';
const session = await (await request.get('/api/auth/session')).json();
while (true) {
const response = await request.get(`/api/sync/pull?cursor=${cursor}&limit=200`, { headers: { 'x-model-user': session.user.id } });
expect(response.ok()).toBe(true);
const result = await response.json(); allChanges.push(...result.changes); cursor = result.nextCursor;
if (!result.hasMore) break;
}
const saved = allChanges.filter(c => c.change.entityType === 'delivery' && c.change.record.notes === offlineNotes);
expect(saved).toHaveLength(1);
expect(`delivery-${saved[0]!.change.record.id}`).toBe(rowId);
expect(saved[0]!.change.record.deliveryNumber).toBeGreaterThan(0);
expect(saved[0]!.change.record.deliverySlot).toBe('afternoon');
const onlineSaved = allChanges.filter(c => c.change.record.notes === onlineNotes);
expect(onlineSaved).toHaveLength(1);
expect(onlineSaved[0]!.change.record).toMatchObject({ deliverySlot: 'custom', specificTime: '14:35' });
for (const viewport of [{ width: 390, height: 844 }, { width: 768, height: 1024 }, { width: 1024, height: 768 }, { width: 1440, height: 900 }]) {
await page.setViewportSize(viewport);
await expect(page.getByRole('button', { name: '+ שמירת משלוח' })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
}
await page.screenshot({ path: 'test-results/model-delivery-desktop.png', fullPage: true });
await page.getByRole('button', { name: 'לוח בקרה', exact: true }).click();
await expect(page.getByRole('region', { name: 'לוח משלוחים שבועי' })).toBeVisible();
await page.screenshot({ path: 'test-results/model-delivery-dashboard.png' });
await page.getByRole('button', { name: 'עלויות', exact: true }).click();
await expect(page.getByRole('heading', { name: 'עלויות משלוחים' })).toBeVisible();
await page.screenshot({ path: 'test-results/model-delivery-costs.png' });
await openDeliveryView(page);
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole('button', { name: 'פתיחת תפריט' })).toBeVisible();
await page.getByRole('button', { name: 'פתיחת תפריט' }).click();
await expect(page.locator('.sidebar')).toHaveClass(/open/);
await page.getByRole('button', { name: 'משלוחים', exact: true }).click();
await expect(page.locator('.sidebar')).not.toHaveClass(/open/);
await page.screenshot({ path: 'test-results/model-delivery-mobile.png', fullPage: true });
expect(browserErrors).toEqual([]);
});
test('creates an offline customer and assigned address before its delivery, then syncs in order', async ({ page, context }) => {
await page.goto('/');
await openDeliveryView(page);
await expect(page.getByRole('combobox', { name: 'סוג משלוח', exact: true }).locator('option')).toHaveCount(3);
await context.setOffline(true);
await expect(page.locator('.connection')).toContainText('ללא חיבור לרשת');
await page.getByRole('button', { name: 'לקוחות', exact: true }).click();
const name = `לקוח בדיקה ${Date.now()}`;
await page.getByRole('button', { name: 'לקוח חדש', exact: true }).first().click();
await page.getByLabel('שם הלקוח').fill(name);
await page.getByLabel('איש קשר').fill('איש קשר לבדיקה');
await page.getByLabel('מספר טלפון').fill('050-1234567');
await page.getByLabel('בוקר', { exact: true }).check();
await page.getByRole('button', { name: 'יצירת לקוח', exact: true }).click();
await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();
await page.getByLabel('שם הכתובת').fill('ראשית');
await page.getByLabel('עיר', { exact: true }).fill('תל אביב');
await page.getByLabel('רחוב', { exact: true }).fill('הבדיקה');
await page.getByLabel('מספר', { exact: true }).fill('7');
await page.getByRole('button', { name: 'הוספת כתובת', exact: true }).click();
await expect(page.getByText('הבדיקה 7, תל אביב', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'משלוחים', exact: true }).click();
await page.getByRole('combobox', { name: 'לקוח', exact: true }).selectOption({ label: name });
await page.getByRole('combobox', { name: 'כתובת למשלוח', exact: true }).selectOption({ index: 1 });
await page.getByRole('button', { name: '+ שמירת משלוח', exact: true }).click();
const row = page.locator('.delivery-row').filter({ hasText: name });
await expect(row).toContainText('ממתין לסנכרון');
await context.setOffline(false);
await expect(row).toContainText('מסונכרן');
await expect(row).toContainText('#');
await expect(page.getByRole('alert')).toHaveCount(0);
});
test('edits and archives a customer while preserving related deliveries', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'לקוחות', exact: true }).click();
await page.getByRole('button', { name: 'לקוח חדש', exact: true }).first().click();
const name = `מרפאת פרופיל ${Date.now()}`;
await page.getByLabel('שם הלקוח').fill(name);
await page.getByLabel('איש קשר').fill('נועה כהן');
await page.getByLabel('מספר טלפון').fill('052-7654321');
await page.getByLabel('בוקר', { exact: true }).check();
await page.getByLabel('תחילת בוקר').fill('08:15');
await page.getByLabel('סיום בוקר').fill('11:45');
await page.getByLabel('הערות ללקוח').fill('כניסה מהחניון');
await page.getByRole('button', { name: 'יצירת לקוח' }).click();
await expect(page.getByRole('heading', { name, exact: true })).toBeVisible();
const clientRow = page.locator('.client-row').filter({ hasText: name });
await expect(clientRow.locator('.sync-dot')).toHaveClass(/synced/);
await page.getByLabel('שם הכתובת').fill('מרפאה');
await page.getByLabel('עיר', { exact: true }).fill('חיפה');
await page.getByLabel('רחוב', { exact: true }).fill('הנמל');
await page.getByLabel('מספר', { exact: true }).fill('12');
await page.getByRole('button', { name: 'הוספת כתובת' }).click();
await expect(page.locator('.associated-row').filter({ hasText: 'הנמל 12, חיפה' }).last().locator('.sync-dot')).toHaveClass(/synced/);
await page.getByRole('button', { name: 'משלוחים', exact: true }).click();
await page.getByRole('combobox', { name: 'לקוח', exact: true }).selectOption({ label: name });
await page.getByRole('combobox', { name: 'כתובת למשלוח', exact: true }).selectOption({ index: 1 });
await page.getByRole('button', { name: '+ שמירת משלוח' }).click();
await expect(page.locator('.delivery-row').filter({ hasText: name })).toContainText('מסונכרן');
await page.getByRole('button', { name: 'לקוחות', exact: true }).click();
await clientRow.click();
await expect(page.getByText(/משלוח #/)).toBeVisible();
await expect(page.getByText('הנמל 12, חיפה', { exact: true })).toBeVisible();
await page.screenshot({ path: 'test-results/model-delivery-clients.png' });
await page.getByRole('button', { name: 'עריכה', exact: true }).click();
await page.getByLabel('איש קשר').fill('נועה לוי');
await page.getByRole('button', { name: 'שמירת שינויים' }).click();
await expect(page.locator('.client-summary strong').filter({ hasText: 'נועה לוי' })).toBeVisible();
await expect(clientRow.locator('.sync-dot')).toHaveClass(/synced/);
await page.getByRole('button', { name: 'הסרה', exact: true }).click();
await expect(page.getByRole('alert')).toContainText('יישמרו ויישארו משויכים');
await page.getByRole('button', { name: 'העברה לארכיון' }).click();
await page.getByLabel('הצגת ארכיון').check();
await expect(clientRow).toBeVisible();
await clientRow.click();
await expect(page.getByText(/משלוח #/)).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect(page.locator('.sidebar')).not.toHaveClass(/open/);
await expect.poll(() => page.locator('.sidebar').evaluate(element => { const rect = element.getBoundingClientRect(); return rect.left >= innerWidth || rect.right <= 0; })).toBe(true);
await page.screenshot({ path: 'test-results/model-delivery-clients-mobile.png' });
await page.getByRole('button', { name: 'פתיחת תפריט' }).click();
await expect(page.locator('.sidebar')).toHaveClass(/open/);
await page.getByRole('button', { name: 'משלוחים', exact: true }).click();
await expect(page.getByRole('combobox', { name: 'לקוח', exact: true }).locator('option', { hasText: name })).toHaveCount(0);
});
test('keeps an offline edit across reload, resolves a concurrent edit explicitly, then cancels', async ({ page, context }) => {
const request = context.request;
await page.goto('/');
await openDeliveryView(page);
await expect(page.getByRole('combobox', { name: 'סוג משלוח', exact: true }).locator('option')).toHaveCount(3);
await page.evaluate(async () => { await navigator.serviceWorker.ready; });
await page.reload();
await openDeliveryView(page);
const conflictNotes = `Conflict browser test ${Date.now()}`;
await deliveryForm(page, conflictNotes);
await page.getByRole('button', { name: '+ שמירת משלוח', exact: true }).click();
await expect(page.locator('.detail-panel')).toContainText(conflictNotes);
const row = page.locator('.delivery-row[aria-pressed="true"]');
await expect(row).toContainText('מסונכרן');
const rowId = (await row.getAttribute('data-testid'))!;
const number = await row.locator('.row-top strong').innerText();
await context.setOffline(true);
await page.getByRole('button', { name: 'עריכת משלוח', exact: true }).click();
await page.getByLabel('הערות מעודכנות').fill('Offline edit to keep');
await page.getByRole('button', { name: 'שמירת שינויים', exact: true }).click();
await expect(page.getByTestId(rowId)).toContainText('ממתין לסנכרון');
await page.reload();
await openDeliveryView(page);
await page.getByTestId(rowId).click();
await expect(page.locator('.detail-panel')).toContainText('Offline edit to keep');
const session = await (await request.get('/api/auth/session')).json();
const headers = { 'x-model-request': '1', 'x-model-user': session.user.id };
const remote = await request.post('/api/sync/push', { headers, data: { operationId: randomUUID(), deviceId: randomUUID(), entityId: rowId.replace('delivery-', ''), baseVersion: 1, kind: 'delivery.update', data: { customerId: '20000000-0000-4000-8000-000000000001', addressId: '20000000-0000-4000-8000-000000000002', notes: 'Other device edit' } } });
expect(remote.ok()).toBe(true);
await context.setOffline(false);
const panel = page.getByRole('region', { name: 'פתרון התנגשות' });
await expect(panel).toBeVisible();
await expect(panel).toContainText('Offline edit to keep');
await expect(panel).toContainText('Other device edit');
await page.getByRole('button', { name: 'החלת השינוי שלי על הגרסה החדשה' }).click();
await expect(page.getByTestId(rowId)).toContainText('מסונכרן');
await expect(panel).toHaveCount(0);
await expect(page.locator('.detail-panel')).toContainText('Offline edit to keep');
await context.setOffline(true);
await page.getByRole('button', { name: 'ביטול משלוח', exact: true }).click();
await page.getByLabel('סיבת הביטול').fill('Cancelled in browser test');
await page.getByRole('button', { name: 'אישור ביטול משלוח', exact: true }).click();
await expect(page.getByTestId(rowId)).toContainText('מבוטל');
await page.reload();
await openDeliveryView(page);
await page.getByTestId(rowId).click();
await expect(page.getByTestId(rowId)).toContainText('ממתין לסנכרון');
await context.setOffline(false);
await expect(page.getByTestId(rowId)).toContainText('מסונכרן');
await expect(page.getByTestId(rowId)).toContainText(number);
await expect(page.getByRole('button', { name: 'עריכת משלוח', exact: true })).toBeDisabled();
await expect(page.locator('.detail-panel')).toContainText('15.02');
});