import {existsSync} from 'node:fs' import {join, resolve} from 'node:path' import { expect, installDetailedScreenshots, isRecord, randomHex, test } from './fixtures' import { extensionApi, extensionFrame, installAndEnableExtension, login } from './extension-helpers' import {SUPPORTCHAT} from './extensions' const supportchatDir = process.env.LNBITS_E2E_SUPPORTCHAT_DIR ?? resolve('data/extensions/supportchat') test.skip( !existsSync(join(supportchatDir, 'config.json')), 'Support Chat E2E requires LNBITS_E2E_SUPPORTCHAT_DIR or data/extensions/supportchat.' ) test('install Support Chat and run the visitor-to-agent support workflow', async ({ page, browser, lnbitsServer }, testInfo) => { await login(page, lnbitsServer) await installAndEnableExtension(page, SUPPORTCHAT) await page.goto('/ext/supportchat') let adminFrame = await extensionFrame(page, 'Support Chat') await expect( adminFrame.getByRole('heading', {name: 'Support Chat'}) ).toBeVisible({timeout: 60_000}) const inboxName = `Playwright Support ${randomHex()}` const inboxForm = adminFrame.locator('#inbox-form') const createInboxButton = inboxForm.getByRole('button', { name: 'Create inbox' }) await expect(createInboxButton).toBeEnabled() await inboxForm.locator('[name="name"]').fill(inboxName) await inboxForm .locator('[name="welcomeMessage"]') .fill('How can the Playwright support team help?') await inboxForm.locator('[name="launcherText"]').fill('Ask Playwright') await inboxForm .locator('[name="offlineMessage"]') .fill('Playwright support is offline; leave a message.') await inboxForm.locator('[name="officeHoursEnabled"]').check() await inboxForm.locator('[name="officeHoursStart"]').fill('0') await inboxForm.locator('[name="officeHoursEnd"]').fill('0') await createInboxButton.click() await expect(adminFrame.getByText(inboxName).first()).toBeVisible({ timeout: 60_000 }) const inbox = await supportInbox(page, inboxName) const publicPath = `/ext/supportchat/i/${encodeURIComponent(String(inbox.id))}` const visitorContext = await browser.newContext({ baseURL: lnbitsServer.baseUrl, viewport: {width: 430, height: 820} }) const visitorPage = await visitorContext.newPage() visitorPage.setDefaultTimeout(60_000) const visitorRecorder = await installDetailedScreenshots( visitorPage, testInfo ) try { await visitorPage.goto(publicPath) let visitorFrame = await extensionFrame(visitorPage, 'Support Chat') await expect(visitorFrame.getByText(inboxName)).toBeVisible() await expect( visitorFrame.getByText('Playwright support is offline; leave a message.') ).toBeVisible() const startForm = visitorFrame.locator('#start-box') await startForm.locator('[name="name"]').fill('Alice Visitor') await startForm .locator('[name="email"]') .fill('alice.playwright@example.com') await startForm.locator('[name="subject"]').fill('Checkout is stuck') await startForm .locator('[name="body"]') .fill('The checkout spinner never finishes.') await startForm.getByTestId('start-conversation').click() await expect(visitorPage).toHaveURL(/\/ext\/supportchat\/c\/[a-f0-9]+$/i, { timeout: 60_000 }) visitorFrame = await extensionFrame(visitorPage, 'Support Chat') await expect( visitorFrame.getByText('The checkout spinner never finishes.') ).toBeVisible() await page.goto('/ext/supportchat') adminFrame = await extensionFrame(page, 'Support Chat') await expect(adminFrame.getByText('Checkout is stuck')).toBeVisible({ timeout: 60_000 }) await expect(adminFrame.getByTestId('unread-count')).toHaveText('1') await adminFrame.getByText('Checkout is stuck').click() await expect( adminFrame.getByText('The checkout spinner never finishes.') ).toBeVisible() await expect( adminFrame .locator('.sc-message--theirs') .filter({hasText: 'The checkout spinner never finishes.'}) ).toHaveCSS('justify-content', 'flex-end') const cannedForm = adminFrame.locator('#canned-reply-form') await cannedForm.locator('[name="title"]').fill('Investigating') await cannedForm .locator('[name="body"]') .fill('Thanks — we are investigating this now.') await cannedForm.getByRole('button', {name: 'Add canned reply'}).click() await expect( adminFrame.getByRole('button', {name: 'Investigating', exact: true}) ).toBeVisible() await adminFrame.getByTestId('conversation-status').selectOption('pending') await adminFrame.getByTestId('conversation-priority').selectOption('urgent') await adminFrame.getByTestId('conversation-tags').fill('checkout, browser') const ticketUpdated = page.waitForResponse( response => response.request().method() === 'PUT' && response.url().includes('/api/v1/ext/supportchat/conversations/') ) await adminFrame.getByRole('button', {name: 'Save ticket'}).click() await ticketUpdated await expect(adminFrame.locator('main.sc-shell')).toHaveAttribute( 'data-loading', '' ) await expect(adminFrame.getByTestId('conversation-status')).toHaveValue( 'pending' ) await expect(adminFrame.getByTestId('conversation-priority')).toHaveValue( 'urgent' ) await expect(adminFrame.getByTestId('conversation-tags')).toHaveValue( 'checkout, browser' ) await adminFrame .getByTestId('internal-note') .fill('Only agents should see this diagnostic note.') await adminFrame.getByRole('button', {name: 'Add internal note'}).click() await expect( adminFrame.getByText('Only agents should see this diagnostic note.') ).toBeVisible() await adminFrame .getByRole('button', {name: 'Investigating', exact: true}) .click() await expect(adminFrame.getByTestId('agent-reply')).toHaveValue( 'Thanks — we are investigating this now.' ) await adminFrame.getByRole('button', {name: 'Send'}).click() await expect( visitorFrame.getByText('Thanks — we are investigating this now.') ).toBeVisible({timeout: 60_000}) await expect( adminFrame .locator('.sc-message--mine') .filter({hasText: 'Thanks — we are investigating this now.'}) ).toHaveCSS('justify-content', 'flex-start') await expect( visitorFrame .locator('.sc-message--theirs') .filter({hasText: 'Thanks — we are investigating this now.'}) ).toHaveCSS('justify-content', 'flex-end') await expect( visitorFrame.getByText('Only agents should see this diagnostic note.') ).toHaveCount(0) await visitorPage.waitForTimeout(1_100) await visitorFrame .locator('#message-box [name="body"]') .fill('It also happens in a private window.') await visitorFrame.getByTestId('visitor-send').click() await expect( adminFrame.getByText('It also happens in a private window.') ).toBeVisible({timeout: 60_000}) await expect(adminFrame.getByTestId('unread-count')).toHaveText('1') const ticketResolved = page.waitForResponse( response => response.request().method() === 'POST' && response.url().includes('/api/v1/ext/supportchat/conversations/') && response.url().endsWith('/resolve') ) await adminFrame.getByRole('button', {name: 'Resolve'}).click() const ticketResolvedResponse = await ticketResolved expect( ticketResolvedResponse.ok(), `Could not resolve ticket: ${await ticketResolvedResponse.text()}` ).toBe(true) await expect(visitorFrame.getByText('resolved').first()).toBeVisible({ timeout: 60_000 }) await expect(adminFrame.locator('main.sc-shell')).toHaveAttribute( 'data-loading', '' ) await expect(adminFrame.getByTestId('conversation-status')).toHaveValue( 'resolved' ) await adminFrame.getByTestId('conversation-status').selectOption('open') const ticketReopened = page.waitForResponse( response => response.request().method() === 'PUT' && response.url().includes('/api/v1/ext/supportchat/conversations/') ) await adminFrame.getByRole('button', {name: 'Save ticket'}).click() const ticketReopenedResponse = await ticketReopened expect( ticketReopenedResponse.ok(), `Could not reopen ticket: ${await ticketReopenedResponse.text()}` ).toBe(true) await expect(adminFrame.locator('main.sc-shell')).toHaveAttribute( 'data-loading', '' ) await visitorPage.reload() visitorFrame = await extensionFrame(visitorPage, 'Support Chat') await expect(visitorFrame.getByText('open').first()).toBeVisible({ timeout: 60_000 }) await visitorPage.goto(publicPath) visitorFrame = await extensionFrame(visitorPage, 'Support Chat') await expect( visitorFrame.getByText('Thanks — we are investigating this now.') ).toBeVisible({timeout: 60_000}) } finally { await visitorRecorder.finish() await visitorContext.close() } }) async function supportInbox( page: Parameters[0], inboxName: string ): Promise> { const response = await extensionApi( page, SUPPORTCHAT.extId, 'GET', '/inboxes?rowsPerPage=100' ) const inboxes = Array.isArray(response.inboxes) ? response.inboxes.filter(isRecord) : [] const inbox = inboxes.find(item => item.name === inboxName) if (!inbox) { throw new Error(`Support inbox not found: ${JSON.stringify(response)}`) } return inbox }