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'); });