docs: 交付文档、维护指引与端到端回归
补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
This commit is contained in:
1 parent
5be5ca3376
commit
308332eb94
216 files changed
+37038
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { test, expect, AI_REPLY } from '@e2e'
|
||||
|
||||
// When the model fails, the customer gets an apology rather than silence or debris,
|
||||
// and the very next message is answered normally.
|
||||
|
||||
const APOLOGY = 'I apologize, but I encountered an error, please try again later.'
|
||||
|
||||
test.describe('when the AI provider fails', () => {
|
||||
test('an error from the provider becomes an apology, and the chat carries on @widget', async ({ agent, llm, widget }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/broken/).fail(500, 'upstream exploded')
|
||||
.otherwise().reply('Back to normal.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('this is broken')
|
||||
await expect(widget.lastAgentMessage()).toHaveText(APOLOGY, AI_REPLY)
|
||||
await widget.send('hello again')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Back to normal.', AI_REPLY)
|
||||
})
|
||||
|
||||
test('a provider that never answers is given up on, with an apology @widget @slow', async ({ agent, llm, widget }) => {
|
||||
// Longer than AGENT_RUN_TIMEOUT (.env.e2e), so the backend stops waiting.
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/broken/).after(25_000).reply('Far too late.')
|
||||
.otherwise().reply('Back to normal.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('this is broken')
|
||||
await expect(widget.lastAgentMessage()).toHaveText(APOLOGY, AI_REPLY)
|
||||
await widget.send('hello again')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Back to normal.', AI_REPLY)
|
||||
await expect(widget.agentMessages.filter({ hasText: 'Far too late.' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('a reply that is not the JSON asked for never reaches the customer raw @widget', async ({ agent, llm, widget }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/broken/).malformed()
|
||||
.otherwise().reply('Back to normal.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('this is broken')
|
||||
await expect(widget.lastAgentMessage()).toHaveText(APOLOGY, AI_REPLY)
|
||||
await widget.send('hello again')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Back to normal.', AI_REPLY)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { test, expect, AI_REPLY, customizeAgent, findPerson } from '@e2e'
|
||||
|
||||
// Each way a visitor reaches the chat, and each again when the first answer that
|
||||
// binds them to a customer goes missing (the socket then gets the page's anonymous
|
||||
// token and the widget has to authorize again).
|
||||
for (const lost of [false, true]) {
|
||||
const when = lost ? ', even when its first authorization goes missing' : ''
|
||||
|
||||
test.describe(`chat styles${lost ? ' (first authorization lost)' : ''}`, () => {
|
||||
test.beforeEach(async ({ widget }) => {
|
||||
if (lost) await widget.loseFirstAuthorization()
|
||||
})
|
||||
|
||||
test(`an email-first agent: the visitor gives an email and gets the answer${when} @widget`, async ({ agent, widget, llm, adminApi }) => {
|
||||
await customizeAgent(adminApi, agent.id, { collect_email: true })
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Thanks, how can I help?'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.openToEmailGate()
|
||||
await widget.giveEmail()
|
||||
await widget.send('Hello')
|
||||
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Thanks, how can I help?', AI_REPLY)
|
||||
expect((await findPerson(adminApi, widget.customer.email)).email).toBe(widget.customer.email)
|
||||
})
|
||||
|
||||
test(`an Ask anything agent answers in its panel, with no email asked${when} @widget`, async ({ agent, widget, llm, adminApi }) => {
|
||||
await customizeAgent(adminApi, agent.id, { chat_style: 'ASK_ANYTHING' })
|
||||
await llm.script(agent, (s) => s.otherwise().reply('We ship worldwide.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.openAskAnything()
|
||||
await widget.ask('Do you ship abroad?')
|
||||
|
||||
await expect(widget.lastAnswer()).toHaveText('We ship worldwide.', AI_REPLY)
|
||||
})
|
||||
|
||||
test.describe('lead capture', () => {
|
||||
test.use({ agentSettings: { leadCapture: true } })
|
||||
|
||||
test(`a visitor with no email asked shares details and becomes a lead${when} @widget @leads`, async ({ agent, widget, llm, adminApi }) => {
|
||||
const { email, name } = widget.customer
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/@/).captureLead('Thanks! Someone from sales will reach out.', { email, name })
|
||||
.otherwise().reply('Happy to help — what is your email?'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('I want a demo')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help — what is your email?', AI_REPLY)
|
||||
await widget.send(`I am ${name}, ${email}`)
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Thanks! Someone from sales will reach out.', AI_REPLY)
|
||||
|
||||
expect((await findPerson(adminApi, email)).lead_stage).toBe('lead')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { test, expect, AI_REPLY, createWidgetApp, customizeAgent, findPerson, issueConversationToken, unique } from '@e2e'
|
||||
|
||||
// An email typed into the widget is not proof of being that customer. Someone else
|
||||
// who types it gets the same person in the inbox, but never their conversations,
|
||||
// and never the right to change who they are.
|
||||
test.describe('conversation ownership', () => {
|
||||
test.beforeEach(async ({ agent, adminApi }) => {
|
||||
await customizeAgent(adminApi, agent.id, { collect_email: true })
|
||||
})
|
||||
|
||||
test('a second browser that gives a customer\'s email starts its own conversation @widget @security', async ({ agent, widget, makeVisitor, llm }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/order/i).reply('Order 1234 ships tomorrow.')
|
||||
.otherwise().reply('Happy to help!'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.openToEmailGate()
|
||||
await widget.giveEmail()
|
||||
await widget.send('Where is my order 1234?')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Order 1234 ships tomorrow.', AI_REPLY)
|
||||
|
||||
const stranger = await makeVisitor(widget.customer)
|
||||
await stranger.embed()
|
||||
await stranger.openToEmailGate()
|
||||
await stranger.giveEmail()
|
||||
await stranger.send('Hello')
|
||||
await expect(stranger.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
// Their reply has arrived, so the history has loaded: only their own message is in it.
|
||||
await expect(stranger.customerMessages).toHaveText(['Hello'])
|
||||
|
||||
await widget.page.reload()
|
||||
await widget.open()
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Order 1234 ships tomorrow.', AI_REPLY)
|
||||
await expect(widget.customerMessages).toHaveText(['Where is my order 1234?'])
|
||||
})
|
||||
|
||||
test.describe('lead capture', () => {
|
||||
test.use({ agentSettings: { leadCapture: true } })
|
||||
|
||||
test('a visitor who only typed a customer\'s email cannot rename them or make them a lead @widget @security @leads', async ({ agent, widget, makeVisitor, llm, adminApi }) => {
|
||||
const { email } = widget.customer
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/Someone Else/).captureLead('Thanks, noted.', { email, name: 'Someone Else' })
|
||||
.otherwise().reply('Happy to help!'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.openToEmailGate()
|
||||
await widget.giveEmail()
|
||||
await widget.send('Hello')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
|
||||
const stranger = await makeVisitor(widget.customer)
|
||||
await stranger.embed()
|
||||
await stranger.openToEmailGate()
|
||||
await stranger.giveEmail()
|
||||
await stranger.send(`I am Someone Else, ${email}`)
|
||||
await expect(stranger.lastAgentMessage()).toHaveText('Thanks, noted.', AI_REPLY)
|
||||
|
||||
const person = await findPerson(adminApi, email)
|
||||
expect(person.name).not.toBe('Someone Else')
|
||||
expect(person.lead_stage).toBe('visitor')
|
||||
})
|
||||
})
|
||||
|
||||
test('an address claimed before its owner signs in gives the claimer none of their chats @widget @security', async ({ agent, widget, makeVisitor, llm, adminApi }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/invoice/i).reply('Your invoice is on its way.')
|
||||
.otherwise().reply('Happy to help!'))
|
||||
const owner = { name: `Owner ${unique('owner')}`, email: `${unique('owner')}@example.com` }
|
||||
|
||||
// Someone claims the address first, through the widget's email gate.
|
||||
const claimer = await makeVisitor(owner)
|
||||
await claimer.embed()
|
||||
await claimer.openToEmailGate()
|
||||
await claimer.giveEmail()
|
||||
await claimer.send('Hello')
|
||||
await expect(claimer.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
|
||||
// Then the site signs its real owner in, and they chat.
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, owner)
|
||||
await widget.embed({ token })
|
||||
await widget.open()
|
||||
await widget.send('Where is my invoice?')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Your invoice is on its way.', AI_REPLY)
|
||||
|
||||
// The claimer comes back: still able to chat, but in a conversation of their own.
|
||||
await claimer.page.reload()
|
||||
await claimer.open()
|
||||
await claimer.send('Anything new?')
|
||||
await expect(claimer.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
await expect(claimer.customerMessages).toHaveText(['Anything new?'])
|
||||
|
||||
await widget.page.reload()
|
||||
await widget.open()
|
||||
await expect(widget.customerMessages).toHaveText(['Where is my invoice?'])
|
||||
})
|
||||
})
|
||||
|
||||
// What scoping a conversation to its browser must not take away.
|
||||
test.describe('a visitor keeps their own conversation', () => {
|
||||
test('a second tab of the same browser joins the same conversation @widget', async ({ agent, widget, llm }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Happy to help!'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('Hello from the first tab')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
|
||||
const tab = await widget.inAnotherTab()
|
||||
await tab.embed()
|
||||
await tab.open()
|
||||
await expect(tab.customerMessages).toHaveText(['Hello from the first tab'])
|
||||
})
|
||||
|
||||
test('a visitor the site signed in finds their conversation in another browser @widget', async ({ agent, widget, makeVisitor, llm, adminApi }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Happy to help!'))
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
|
||||
await widget.embed({ token: await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer) })
|
||||
await widget.open()
|
||||
await widget.send('Started on my laptop')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
|
||||
const phone = await makeVisitor(widget.customer)
|
||||
await phone.embed({ token: await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer) })
|
||||
await phone.open()
|
||||
await expect(phone.customerMessages).toHaveText(['Started on my laptop'])
|
||||
})
|
||||
|
||||
test('a site that signs the visitor in with identify() hands the widget their conversation @widget', async ({ agent, widget, makeVisitor, llm, adminApi }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Happy to help!'))
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
|
||||
await widget.embed({ token: await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer) })
|
||||
await widget.open()
|
||||
await widget.send('Signed-in question')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
|
||||
// The same person opens the site signed out, then signs in on the page.
|
||||
const later = await makeVisitor(widget.customer)
|
||||
await later.embed()
|
||||
await later.open()
|
||||
await later.api('identify', await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer))
|
||||
await later.open()
|
||||
await expect(later.customerMessages).toHaveText(['Signed-in question'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { test, expect, AI_REPLY } from '@e2e'
|
||||
|
||||
// The dashboard also offers an iframe snippet: the chat page itself, served by
|
||||
// the API. Its socket comes from the API's own origin, which must be allowed
|
||||
// whether or not the operator listed it in CORS_ORIGINS.
|
||||
|
||||
test('a customer chats through the iframe snippet @widget', async ({ agent, widget, llm }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Hello from the iframe'))
|
||||
|
||||
await widget.embed({ snippet: 'iframe' })
|
||||
await widget.expectReady()
|
||||
await widget.send('Hi')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Hello from the iframe', AI_REPLY)
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@e2e'
|
||||
|
||||
test.describe('widget JS API', () => {
|
||||
test('the host page opens and closes the chat with window.ChatterMate @widget', async ({ widget }) => {
|
||||
await widget.embed()
|
||||
|
||||
await widget.api('open')
|
||||
await expect(widget.panel).toHaveClass(/\bactive\b/)
|
||||
await widget.expectReady()
|
||||
expect(await widget.api('isOpen')).toBe(true)
|
||||
|
||||
await widget.api('close')
|
||||
await expect(widget.panel).not.toHaveClass(/\bactive\b/)
|
||||
expect(await widget.api('isOpen')).toBe(false)
|
||||
})
|
||||
|
||||
test('a site can hide the launcher and open the chat from its own button @widget', async ({ widget }) => {
|
||||
await widget.embed({ config: { launcher: false }, withTriggerButton: true })
|
||||
await expect(widget.launcher).toBeHidden()
|
||||
|
||||
await widget.hostChatButton.click()
|
||||
await expect(widget.panel).toHaveClass(/\bactive\b/)
|
||||
await widget.expectReady()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { test, expect, customizeAgent, expectUnchanged } from '@e2e'
|
||||
|
||||
// Where the widget sits and how it looks: the site's own config, the dashboard's
|
||||
// display settings, and which one wins.
|
||||
|
||||
test.describe('widget placement', () => {
|
||||
test('the launcher goes on the left when the site asks for it @widget', async ({ widget }) => {
|
||||
await widget.embed({ config: { position: { side: 'left' } } })
|
||||
const box = (await widget.launcher.boundingBox())!
|
||||
expect(box.x).toBeLessThan(100)
|
||||
})
|
||||
|
||||
// The suite runs with reduced motion, which the loader honours by not sliding the
|
||||
// panel in, so its final position is there at once.
|
||||
test('sidebar mode opens the chat as a full-height panel on the right @widget', async ({ widget }) => {
|
||||
await widget.embed({ config: { displayMode: 'sidebar-right' } })
|
||||
await widget.open()
|
||||
const viewport = widget.page.viewportSize()!
|
||||
const panel = (await widget.panel.boundingBox())!
|
||||
expect(panel.height).toBeGreaterThanOrEqual(viewport.height - 1)
|
||||
expect(Math.round(panel.x + panel.width)).toBe(viewport.width)
|
||||
})
|
||||
|
||||
test('the dashboard\'s display settings apply without any code, and the site\'s own config wins @widget', async ({ agent, adminApi, makeWidget }) => {
|
||||
await customizeAgent(adminApi, agent.id, { customization_metadata: { widget_display: { side: 'left' } } })
|
||||
|
||||
// The loader applies the dashboard's settings once it has fetched them.
|
||||
const fromDashboard = await makeWidget(agent.id)
|
||||
await fromDashboard.embed()
|
||||
await expect.poll(async () => (await fromDashboard.launcher.boundingBox())?.x).toBeLessThan(100)
|
||||
|
||||
const siteOverrides = await makeWidget(agent.id)
|
||||
await siteOverrides.embed({ config: { position: { side: 'right' } } })
|
||||
const viewport = siteOverrides.page.viewportSize()!
|
||||
await expect(siteOverrides.launcher).toBeVisible()
|
||||
await expect.poll(async () => (await siteOverrides.launcher.boundingBox())?.x).toBeGreaterThan(viewport.width - 150)
|
||||
// …and stays there once the dashboard's settings arrive.
|
||||
await expectUnchanged('launcher position', async () => Math.round((await siteOverrides.launcher.boundingBox())!.x))
|
||||
})
|
||||
|
||||
test('the agent\'s welcome message and launcher colour show in the widget @widget', async ({ agent, adminApi, widget }) => {
|
||||
await customizeAgent(adminApi, agent.id, { welcome_message: 'Hi from Acme support — how can we help?', chat_bubble_color: '#123456' })
|
||||
|
||||
await widget.embed()
|
||||
await expect(widget.launcher).toHaveCSS('background-color', 'rgb(18, 52, 86)')
|
||||
await widget.open()
|
||||
await expect(widget.welcomeMessage).toHaveText('Hi from Acme support — how can we help?')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { test, expect } from '@e2e'
|
||||
|
||||
interface AgentPerformance { bot_agents: Array<{ id: string; avg_rating: number; rating_count: number }> }
|
||||
|
||||
test.describe('chat rating', () => {
|
||||
test.use({ agentSettings: { askForRating: true } })
|
||||
|
||||
test('a customer rates the chat when the agent wraps up, and the rating is recorded against the agent @widget', async ({ agent, widget, llm, adminApi }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/that is all|thanks/i).endChat('Glad I could help. Goodbye!')
|
||||
.otherwise().reply('Sure thing.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('That is all, thanks')
|
||||
|
||||
await expect(widget.ratingPrompt).toBeVisible()
|
||||
await widget.rate(4)
|
||||
await expect(widget.ratingThanks).toBeVisible()
|
||||
|
||||
// The thanks is client-side; the agent's numbers are what the team actually sees.
|
||||
const ratingOf = async () => (await adminApi.get<AgentPerformance>('/analytics/agent-performance', { params: { time_range: '24h' } }))
|
||||
.bot_agents.find((a) => a.id === agent.id)
|
||||
await expect.poll(ratingOf).toMatchObject({ rating_count: 1, avg_rating: 4 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test, expect, ApiClient, createWidgetApp, issueConversationToken, unique, AI_REPLY } from '@e2e'
|
||||
|
||||
test.describe('token-authenticated widget', () => {
|
||||
test.use({ agentSettings: { requireTokenAuth: true } })
|
||||
|
||||
test('a visitor with a token from the site\'s server can chat @widget', async ({ agent, widget, llm, adminApi }) => {
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer)
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Welcome back!'))
|
||||
|
||||
await widget.embed({ token })
|
||||
await widget.open()
|
||||
await widget.send('Hi')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Welcome back!', AI_REPLY)
|
||||
})
|
||||
|
||||
test('without a token the widget refuses to load @widget', async ({ widget }) => {
|
||||
await widget.embed()
|
||||
// The notice lives in the chat panel, so the visitor sees it when they click the launcher.
|
||||
await widget.launcher.click()
|
||||
await expect(widget.unavailableNotice).toBeVisible()
|
||||
})
|
||||
|
||||
test('a tampered token is refused @widget @security', async ({ widget, adminApi }) => {
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer)
|
||||
// Swap the signature for another token's: a well-formed JWT this widget never signed.
|
||||
const other = await issueConversationToken(adminApi, apiKey, widget.widgetId, { ...widget.customer, email: `x.${widget.customer.email}` })
|
||||
const tampered = `${token.split('.').slice(0, 2).join('.')}.${other.split('.')[2]}`
|
||||
|
||||
await widget.embed({ token: tampered })
|
||||
await widget.launcher.click()
|
||||
await expect(widget.unavailableNotice).toBeVisible()
|
||||
})
|
||||
|
||||
test('a token minted for another widget is refused @widget @security', async ({ widget, adminApi, agent, makeWidget }) => {
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
const otherWidget = await makeWidget(agent.id)
|
||||
const tokenForOther = await issueConversationToken(adminApi, apiKey, otherWidget.widgetId, widget.customer)
|
||||
|
||||
await widget.embed({ token: tokenForOther })
|
||||
await widget.launcher.click()
|
||||
await expect(widget.unavailableNotice).toBeVisible()
|
||||
})
|
||||
|
||||
test('an expired token no longer opens the chat @widget @security @slow', async ({ widget, adminApi, request }) => {
|
||||
test.slow()
|
||||
const { apiKey } = await createWidgetApp(adminApi, unique('acme-site'))
|
||||
const token = await issueConversationToken(adminApi, apiKey, widget.widgetId, widget.customer, 60)
|
||||
// What the loader fetches to render the chat frame for this visitor. Each
|
||||
// poll on a fresh connection: the backend drops a keep-alive connection
|
||||
// idle for 5s, the poll interval, and a reused one can hang up mid-request.
|
||||
const visitor = new ApiClient(request, token)
|
||||
const frame = () => visitor.raw('GET', `/widgets/${widget.widgetId}/data`, { headers: { Connection: 'close' } })
|
||||
expect((await frame()).status()).toBe(200)
|
||||
|
||||
// The shortest lifetime the API allows; the backend's own clock decides.
|
||||
await expect.poll(async () => (await frame()).status(), { timeout: 90_000, intervals: [5_000] }).toBe(401)
|
||||
expect(JSON.stringify(await (await frame()).json())).toContain('Conversation token expired')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { test, expect, AI_REPLY } from '@e2e'
|
||||
|
||||
test.describe('widget chat', () => {
|
||||
test('a customer gets the agent\'s answer @smoke @widget', async ({ agent, widget, llm }) => {
|
||||
await llm.script(agent, (s) => s
|
||||
.when(/opening hours/i).reply('We are open 9am to 5pm, Monday to Friday.')
|
||||
.otherwise().reply('Happy to help!'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('What are your opening hours?')
|
||||
|
||||
await expect(widget.lastAgentMessage()).toHaveText('We are open 9am to 5pm, Monday to Friday.', AI_REPLY)
|
||||
})
|
||||
|
||||
test('the conversation is still there after the customer reloads the page @widget', async ({ agent, widget, llm }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Your order ships tomorrow.'))
|
||||
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('Where is my order?')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Your order ships tomorrow.', AI_REPLY)
|
||||
|
||||
await widget.page.reload()
|
||||
await widget.open()
|
||||
await expect(widget.customerMessages.last()).toHaveText('Where is my order?')
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Your order ships tomorrow.', AI_REPLY)
|
||||
})
|
||||
|
||||
test('a visitor whose first authorization went missing can still chat @widget', async ({ agent, widget, llm }) => {
|
||||
await llm.script(agent, (s) => s.otherwise().reply('Happy to help!'))
|
||||
|
||||
// The socket refuses the anonymous token; the widget authorizes again and connects.
|
||||
await widget.loseFirstAuthorization()
|
||||
await widget.embed()
|
||||
await widget.open()
|
||||
await widget.send('Hello?')
|
||||
|
||||
await expect(widget.lastAgentMessage()).toHaveText('Happy to help!', AI_REPLY)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user