@adatechnology/conversations-ui 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
- package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
- package/dist/{chunk-4P5KMWOI.js → chunk-FAQLC3TX.js} +327 -522
- package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
- package/dist/chunk-MRVFFJEH.js +64 -0
- package/dist/chunk-O6457XHK.js +255 -0
- package/dist/chunk-OVFCLRQQ.js +84 -0
- package/dist/flows/index.js +5 -3
- package/dist/index.d.ts +21 -5
- package/dist/index.js +783 -807
- package/dist/participant/index.d.ts +186 -0
- package/dist/participant/index.js +1708 -0
- package/dist/preview/index.d.ts +4 -2
- package/dist/preview/index.js +4 -2
- package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
- package/dist/styles.css +689 -0
- package/package.json +8 -4
- package/src/ChannelIcon.tsx +2 -0
- package/src/MessageText.test.tsx +76 -0
- package/src/MessageText.tsx +28 -12
- package/src/MessageTextCopiedBadge.tsx +15 -0
- package/src/StatusTicks.test.tsx +51 -0
- package/src/StatusTicks.tsx +36 -6
- package/src/buildOutput.test.ts +41 -0
- package/src/channelCapability.test.ts +17 -0
- package/src/conversationChannel.test.ts +30 -1
- package/src/conversationChannel.ts +19 -1
- package/src/legacyChannels.golden.test.ts +89 -0
- package/src/legacyDefaults.golden.test.tsx +127 -0
- package/src/legacyStyles.fixture.css +1121 -0
- package/src/legacyStyles.golden.test.ts +121 -0
- package/src/messageTextCopyInteraction.test.ts +75 -0
- package/src/messageTextCopyInteraction.ts +37 -0
- package/src/participant/ParticipantAttachmentItem.tsx +77 -0
- package/src/participant/ParticipantChannelBadges.test.tsx +59 -0
- package/src/participant/ParticipantChannelBadges.tsx +75 -0
- package/src/participant/ParticipantComposer.test.tsx +97 -0
- package/src/participant/ParticipantComposer.tsx +167 -0
- package/src/participant/ParticipantConversations.test.tsx +90 -0
- package/src/participant/ParticipantConversations.tsx +49 -0
- package/src/participant/ParticipantConversationsScreen.tsx +78 -0
- package/src/participant/ParticipantInbox.test.tsx +244 -0
- package/src/participant/ParticipantInbox.tsx +149 -0
- package/src/participant/ParticipantInboxRow.tsx +75 -0
- package/src/participant/ParticipantInboxSearchField.tsx +34 -0
- package/src/participant/ParticipantLoadState.tsx +27 -0
- package/src/participant/ParticipantMessageBubble.test.tsx +167 -0
- package/src/participant/ParticipantMessageBubble.tsx +185 -0
- package/src/participant/ParticipantProtocolBadge.tsx +74 -0
- package/src/participant/ParticipantProtocolView.test.tsx +208 -0
- package/src/participant/ParticipantThread.test.tsx +149 -0
- package/src/participant/ParticipantThread.tsx +184 -0
- package/src/participant/ParticipantThreadScreen.tsx +135 -0
- package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
- package/src/participant/bindAttachmentUrlResolver.ts +7 -0
- package/src/participant/index.ts +26 -0
- package/src/participant/participant.types.ts +52 -0
- package/src/participant/participantApi.types.ts +59 -0
- package/src/participant/participantAttachmentUrl.test.ts +32 -0
- package/src/participant/participantAttachmentUrl.ts +10 -0
- package/src/participant/participantBubbleActions.test.ts +44 -0
- package/src/participant/participantBubbleActions.ts +40 -0
- package/src/participant/participantChannels.test.ts +42 -0
- package/src/participant/participantChannels.ts +33 -0
- package/src/participant/participantDrafts.test.ts +65 -0
- package/src/participant/participantDrafts.ts +50 -0
- package/src/participant/participantFileValidation.test.ts +86 -0
- package/src/participant/participantFileValidation.ts +72 -0
- package/src/participant/participantFixtures.test-helper.ts +41 -0
- package/src/participant/participantGrouping.test.ts +236 -0
- package/src/participant/participantGrouping.ts +90 -0
- package/src/participant/participantInboxState.test.ts +69 -0
- package/src/participant/participantInboxState.ts +74 -0
- package/src/participant/participantLabels.ts +115 -0
- package/src/participant/participantLoadView.test.ts +23 -0
- package/src/participant/participantLoadView.ts +21 -0
- package/src/participant/participantMessages.test.ts +128 -0
- package/src/participant/participantMessages.ts +73 -0
- package/src/participant/participantNewMessages.test.ts +54 -0
- package/src/participant/participantNewMessages.ts +29 -0
- package/src/participant/participantProtocol.test.ts +104 -0
- package/src/participant/participantProtocol.ts +52 -0
- package/src/participant/participantProtocolCopy.test.ts +45 -0
- package/src/participant/participantProtocolCopy.ts +32 -0
- package/src/participant/participantQuickReply.test.ts +23 -0
- package/src/participant/participantQuickReply.ts +6 -0
- package/src/participant/participantScroll.test.ts +69 -0
- package/src/participant/participantScroll.ts +43 -0
- package/src/participant/participantSendController.test.ts +201 -0
- package/src/participant/participantSendController.ts +134 -0
- package/src/participant/participantSendStates.ts +31 -0
- package/src/participant/participantTheme.ts +20 -0
- package/src/participant/participantThreadState.test.ts +75 -0
- package/src/participant/participantThreadState.ts +59 -0
- package/src/participant/shouldMarkRead.test.ts +24 -0
- package/src/participant/shouldMarkRead.ts +16 -0
- package/src/participant/useDocumentVisibility.ts +18 -0
- package/src/participant/useParticipantConversationView.ts +60 -0
- package/src/participant/useParticipantDraft.ts +45 -0
- package/src/participant/useParticipantInbox.ts +57 -0
- package/src/participant/useParticipantMarkRead.ts +26 -0
- package/src/participant/useParticipantRevalidation.ts +36 -0
- package/src/participant/useParticipantSend.ts +84 -0
- package/src/participant/useParticipantThread.ts +116 -0
- package/src/participant/useParticipantThreadScroll.ts +26 -0
- package/src/participant/useStickToBottom.ts +77 -0
- package/src/preview/ConversationSimulatorPanel.tsx +8 -0
- package/src/styles.css +178 -0
- package/src/types.ts +3 -1
package/src/ChannelIcon.tsx
CHANGED
|
@@ -14,6 +14,8 @@ export const CHANNEL_BRAND_COLOR: Readonly<Record<ConversationChannel, string>>
|
|
|
14
14
|
[CONVERSATION_CHANNEL.WHATSAPP]: '#25D366',
|
|
15
15
|
[CONVERSATION_CHANNEL.MESSENGER]: '#0084FF',
|
|
16
16
|
[CONVERSATION_CHANNEL.INSTAGRAM]: '#E4405F',
|
|
17
|
+
[CONVERSATION_CHANNEL.APP]: '#7C3AED',
|
|
18
|
+
[CONVERSATION_CHANNEL.PORTAL]: '#0F766E',
|
|
17
19
|
[CONVERSATION_CHANNEL.WEBCHAT]: '#64748B',
|
|
18
20
|
[CONVERSATION_CHANNEL.EMAIL]: '#EA4335',
|
|
19
21
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { MessageText } from './MessageText'
|
|
5
|
+
import { MessageTextCopiedBadge } from './MessageTextCopiedBadge'
|
|
6
|
+
import type { MessagePayload } from './types'
|
|
7
|
+
|
|
8
|
+
const TEXT_MESSAGE: MessagePayload = {
|
|
9
|
+
id: 'msg-1',
|
|
10
|
+
type: 'text',
|
|
11
|
+
direction: 'inbound',
|
|
12
|
+
sender: 'customer',
|
|
13
|
+
timestamp: '2026-08-05T12:00:00.000Z',
|
|
14
|
+
content: 'Olá *mundo*',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('MessageText', () => {
|
|
18
|
+
it('com appearance="stylesheet" usa a classe cv-message-text e nenhuma utilitária Tailwind', () => {
|
|
19
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} appearance="stylesheet" />)
|
|
20
|
+
|
|
21
|
+
expect(markup).toContain('cv-message-text')
|
|
22
|
+
expect(markup).not.toContain('whitespace-pre-wrap')
|
|
23
|
+
expect(markup).not.toContain('select-all')
|
|
24
|
+
expect(markup).not.toContain('text-[')
|
|
25
|
+
expect(markup).not.toContain('[&_')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('o badge em stylesheet usa a classe cv-message-text__copied e nenhuma utilitária Tailwind', () => {
|
|
29
|
+
const markup = renderToStaticMarkup(<MessageTextCopiedBadge appearance="stylesheet" />)
|
|
30
|
+
|
|
31
|
+
expect(markup).toContain('cv-message-text__copied')
|
|
32
|
+
expect(markup).not.toContain('absolute')
|
|
33
|
+
expect(markup).not.toContain('bg-[')
|
|
34
|
+
expect(markup).not.toContain('px-1.5')
|
|
35
|
+
expect(markup).not.toContain('-translate-y-full')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('por padrão é o div da 0.4.2: classes Tailwind, sem role nem tabindex', () => {
|
|
39
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} />)
|
|
40
|
+
|
|
41
|
+
expect(markup).toContain('select-all')
|
|
42
|
+
expect(markup).toContain('whitespace-pre-wrap')
|
|
43
|
+
expect(markup).not.toContain('role=')
|
|
44
|
+
expect(markup).not.toContain('tabindex')
|
|
45
|
+
expect(markup).not.toContain('cv-message-text')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('com accessibleCopy liga role=button e tabindex', () => {
|
|
49
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} accessibleCopy />)
|
|
50
|
+
|
|
51
|
+
expect(markup).toContain('role="button"')
|
|
52
|
+
expect(markup).toContain('tabindex="0"')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('em stylesheet a classe copiável (user-select) só aparece quando copia', () => {
|
|
56
|
+
const markup = renderToStaticMarkup(<MessageText message={TEXT_MESSAGE} appearance="stylesheet" />)
|
|
57
|
+
|
|
58
|
+
expect(markup).toContain('cv-message-text--copyable')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('com copyOnClick={false} em stylesheet não copia: sem role, sem tabindex e sem classe copiável', () => {
|
|
62
|
+
const markup = renderToStaticMarkup(
|
|
63
|
+
<MessageText message={TEXT_MESSAGE} copyOnClick={false} accessibleCopy appearance="stylesheet" />,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(markup).not.toContain('role="button"')
|
|
67
|
+
expect(markup).not.toContain('tabindex')
|
|
68
|
+
expect(markup).not.toContain('cv-message-text--copyable')
|
|
69
|
+
expect(markup).toContain('class="cv-message-text"')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('o badge usa "Copiado!" por padrão e aceita outro rótulo', () => {
|
|
73
|
+
expect(renderToStaticMarkup(<MessageTextCopiedBadge />)).toContain('Copiado!')
|
|
74
|
+
expect(renderToStaticMarkup(<MessageTextCopiedBadge label="Copied" />)).toContain('Copied')
|
|
75
|
+
})
|
|
76
|
+
})
|
package/src/MessageText.tsx
CHANGED
|
@@ -1,41 +1,57 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react'
|
|
2
2
|
import { CHAT_TEXT_SECONDARY_CLASS } from './theme'
|
|
3
3
|
import type { MessagePayload } from './types'
|
|
4
|
-
import { useConversations } from './providers/ConversationsProvider'
|
|
5
4
|
import { parseWhatsAppFormatting } from './lib/whatsapp-formatting'
|
|
5
|
+
import { resolveCopyInteraction } from './messageTextCopyInteraction'
|
|
6
|
+
import { MessageTextCopiedBadge, type MessageTextAppearance } from './MessageTextCopiedBadge'
|
|
6
7
|
|
|
7
8
|
export interface MessageTextProps {
|
|
8
9
|
message: MessagePayload
|
|
10
|
+
/** Defaults to true (tap copies the text, as in 0.4.2); false disables copying. */
|
|
11
|
+
copyOnClick?: boolean
|
|
12
|
+
/** Opt-in: exposes the copy target as a keyboard-operable button (role, tabIndex, Enter/Space). Defaults to false. */
|
|
13
|
+
accessibleCopy?: boolean
|
|
14
|
+
/** Text of the badge shown after copying. Defaults to 'Copiado!', as in 0.4.2. */
|
|
15
|
+
copiedLabel?: string
|
|
16
|
+
/** 'tailwind' (default) keeps the 0.4.2 utility classes; 'stylesheet' uses the .cv-* rules from styles.css. */
|
|
17
|
+
appearance?: MessageTextAppearance
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
const TAILWIND_CLASS =
|
|
21
|
+
'text-[14.2px] leading-[19px] whitespace-pre-wrap break-words select-all [&_strong]:font-bold [&_em]:italic [&_del]:line-through'
|
|
22
|
+
|
|
23
|
+
function resolveClassName(appearance: MessageTextAppearance, copyOnClick: boolean): string {
|
|
24
|
+
if (appearance === 'tailwind') return TAILWIND_CLASS
|
|
25
|
+
return copyOnClick ? 'cv-message-text cv-message-text--copyable' : 'cv-message-text'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function MessageText({
|
|
29
|
+
message, copyOnClick = true, accessibleCopy = false, copiedLabel, appearance = 'tailwind',
|
|
30
|
+
}: MessageTextProps) {
|
|
12
31
|
const [copied, setCopied] = useState(false)
|
|
13
32
|
|
|
14
33
|
const handleCopy = useCallback(async () => {
|
|
15
|
-
if (!message.content) return
|
|
34
|
+
if (!copyOnClick || !message.content) return
|
|
16
35
|
try {
|
|
17
36
|
await navigator.clipboard.writeText(message.content)
|
|
18
37
|
setCopied(true)
|
|
19
38
|
setTimeout(() => setCopied(false), 2000)
|
|
20
39
|
} catch {
|
|
21
|
-
// Clipboard not available
|
|
22
40
|
}
|
|
23
|
-
}, [message.content])
|
|
41
|
+
}, [copyOnClick, message.content])
|
|
24
42
|
|
|
25
43
|
if (message.type === 'template')
|
|
26
44
|
return <p className={`text-sm italic ${CHAT_TEXT_SECONDARY_CLASS}`}>{message.content ?? 'Template message'}</p>
|
|
27
45
|
|
|
46
|
+
const interactionProps = resolveCopyInteraction({ copyOnClick, accessibleCopy, onCopy: handleCopy })
|
|
47
|
+
|
|
28
48
|
return (
|
|
29
49
|
<div
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
className={resolveClassName(appearance, copyOnClick)}
|
|
51
|
+
{...interactionProps}
|
|
32
52
|
>
|
|
33
53
|
<div>{parseWhatsAppFormatting(message.content ?? '')}</div>
|
|
34
|
-
{copied &&
|
|
35
|
-
<span className="absolute top-0 right-0 -translate-y-full bg-[#3b4a54] text-white text-[11px] px-1.5 py-0.5 rounded shadow-lg">
|
|
36
|
-
Copiado!
|
|
37
|
-
</span>
|
|
38
|
-
)}
|
|
54
|
+
{copied && <MessageTextCopiedBadge label={copiedLabel} appearance={appearance} />}
|
|
39
55
|
</div>
|
|
40
56
|
)
|
|
41
57
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type MessageTextAppearance = 'tailwind' | 'stylesheet'
|
|
2
|
+
|
|
3
|
+
export interface MessageTextCopiedBadgeProps {
|
|
4
|
+
label?: string
|
|
5
|
+
/** 'tailwind' (default) keeps the 0.4.2 utility classes; 'stylesheet' uses the .cv-* rules from styles.css. */
|
|
6
|
+
appearance?: MessageTextAppearance
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TAILWIND_CLASS =
|
|
10
|
+
'absolute top-0 right-0 -translate-y-full bg-[#3b4a54] text-white text-[11px] px-1.5 py-0.5 rounded shadow-lg'
|
|
11
|
+
|
|
12
|
+
export function MessageTextCopiedBadge({ label = 'Copiado!', appearance = 'tailwind' }: MessageTextCopiedBadgeProps) {
|
|
13
|
+
const className = appearance === 'stylesheet' ? 'cv-message-text__copied' : TAILWIND_CLASS
|
|
14
|
+
return <span className={className}>{label}</span>
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { StatusTicks } from './StatusTicks'
|
|
5
|
+
|
|
6
|
+
const TAILWIND_UTILITIES = ['text-sky-500', 'text-red-500', 'text-black/40', 'flex', 'cursor-help', 'leading-none']
|
|
7
|
+
|
|
8
|
+
describe('StatusTicks', () => {
|
|
9
|
+
it('em stylesheet um status desconhecido cai em sent (um tique)', () => {
|
|
10
|
+
const markup = renderToStaticMarkup(<StatusTicks status="xyz" appearance="stylesheet" />)
|
|
11
|
+
|
|
12
|
+
expect(markup).toContain('cv-status-ticks--sent')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('em stylesheet renderiza o estado queued com relógio e rótulo acessível', () => {
|
|
16
|
+
const markup = renderToStaticMarkup(<StatusTicks status="queued" appearance="stylesheet" />)
|
|
17
|
+
|
|
18
|
+
expect(markup).toContain('cv-status-ticks--queued')
|
|
19
|
+
expect(markup).toContain('aria-label="Queued"')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('em stylesheet o rótulo de queued é configurável', () => {
|
|
23
|
+
const markup = renderToStaticMarkup(<StatusTicks status="queued" appearance="stylesheet" queuedLabel="Na fila" />)
|
|
24
|
+
|
|
25
|
+
expect(markup).toContain('aria-label="Na fila"')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('em stylesheet trata bounced como failed', () => {
|
|
29
|
+
const markup = renderToStaticMarkup(<StatusTicks status="bounced" appearance="stylesheet" />)
|
|
30
|
+
|
|
31
|
+
expect(markup).toContain('cv-status-ticks--failed')
|
|
32
|
+
expect(markup).not.toContain('cv-status-ticks--bounced')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('por padrão mantém as utilitárias Tailwind e nenhum aria-label (0.4.2)', () => {
|
|
36
|
+
const markup = renderToStaticMarkup(<StatusTicks status="read" />)
|
|
37
|
+
|
|
38
|
+
expect(markup).toContain('text-sky-500')
|
|
39
|
+
expect(markup).toContain('cursor-help')
|
|
40
|
+
expect(markup).not.toContain('cv-status-ticks')
|
|
41
|
+
expect(markup).not.toContain('aria-label')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it.each(['sent', 'delivered', 'read', 'failed', 'queued'])('em stylesheet usa só classes cv-* em %s', (status) => {
|
|
45
|
+
const markup = renderToStaticMarkup(<StatusTicks status={status} appearance="stylesheet" />)
|
|
46
|
+
|
|
47
|
+
expect(markup).toContain(`cv-status-ticks--${status}`)
|
|
48
|
+
for (const utility of TAILWIND_UTILITIES) expect(markup).not.toContain(utility)
|
|
49
|
+
expect(markup).not.toContain('text-[')
|
|
50
|
+
})
|
|
51
|
+
})
|
package/src/StatusTicks.tsx
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { AlertTriangle } from 'lucide-react'
|
|
1
|
+
import { AlertTriangle, Clock } from 'lucide-react'
|
|
2
2
|
|
|
3
3
|
export interface StatusTicksProps {
|
|
4
4
|
status: string
|
|
5
5
|
title?: string
|
|
6
|
+
/** 'tailwind' (default) keeps the 0.4.2 markup; 'stylesheet' uses .cv-status-ticks--* and adds queued/bounced handling. */
|
|
7
|
+
appearance?: 'tailwind' | 'stylesheet'
|
|
8
|
+
/** Accessible label of the queued state (stylesheet appearance only). Defaults to 'Queued'. */
|
|
9
|
+
queuedLabel?: string
|
|
6
10
|
}
|
|
7
11
|
|
|
8
|
-
const
|
|
12
|
+
const TAILWIND_COLOR_CLASS: Record<string, string> = {
|
|
9
13
|
sent: 'text-black/40 dark:text-white/40',
|
|
10
14
|
delivered: 'text-black/40 dark:text-white/40',
|
|
11
15
|
read: 'text-sky-500',
|
|
12
16
|
failed: 'text-red-500',
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
const STYLESHEET_MODIFIER: Record<string, string> = {
|
|
20
|
+
queued: 'queued',
|
|
21
|
+
sent: 'sent',
|
|
22
|
+
delivered: 'delivered',
|
|
23
|
+
read: 'read',
|
|
24
|
+
failed: 'failed',
|
|
25
|
+
bounced: 'failed',
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
function Ticks({ double }: { double: boolean }) {
|
|
16
29
|
return (
|
|
17
30
|
<svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -26,10 +39,27 @@ function Ticks({ double }: { double: boolean }) {
|
|
|
26
39
|
)
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
42
|
+
function StylesheetStatusTicks({ status, title, queuedLabel }: { status: string; title?: string; queuedLabel: string }) {
|
|
43
|
+
const modifier = STYLESHEET_MODIFIER[status] ?? 'sent'
|
|
44
|
+
const ariaLabel = modifier === 'queued' ? queuedLabel : undefined
|
|
45
|
+
const icon =
|
|
46
|
+
modifier === 'failed' ? <AlertTriangle size={11} /> : modifier === 'queued' ? <Clock size={11} /> : <Ticks double={modifier !== 'sent'} />
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
className={`cv-status-ticks cv-status-ticks--${modifier}`}
|
|
51
|
+
data-cv-tooltip={title ?? ariaLabel}
|
|
52
|
+
aria-label={ariaLabel}
|
|
53
|
+
>
|
|
54
|
+
{icon}
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function StatusTicks({ status, title, appearance = 'tailwind', queuedLabel = 'Queued' }: StatusTicksProps) {
|
|
60
|
+
if (appearance === 'stylesheet') return <StylesheetStatusTicks status={status} title={title} queuedLabel={queuedLabel} />
|
|
61
|
+
|
|
62
|
+
const colorClass = TAILWIND_COLOR_CLASS[status] ?? TAILWIND_COLOR_CLASS.sent
|
|
33
63
|
|
|
34
64
|
return (
|
|
35
65
|
<span className={`cursor-help leading-none flex items-center ${colorClass}`} data-cv-tooltip={title}>
|
package/src/buildOutput.test.ts
CHANGED
|
@@ -77,3 +77,44 @@ describe('o bundle do host não paga por @xyflow/react sem pedir', () => {
|
|
|
77
77
|
expect(await distText('index.js')).not.toContain('@xyflow/react')
|
|
78
78
|
})
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
describe('a visão do participante é leve e isolada', () => {
|
|
82
|
+
const PARTICIPANT_ENTRY_LIMIT_BYTES = 250 * 1024
|
|
83
|
+
|
|
84
|
+
async function participantBundle(): Promise<string> {
|
|
85
|
+
// O entry mais os chunks relativos que ele importa, transitivamente: é o que o host baixa.
|
|
86
|
+
const seen = new Set<string>()
|
|
87
|
+
const queue = ['participant/index.js']
|
|
88
|
+
let total = ''
|
|
89
|
+
while (queue.length > 0) {
|
|
90
|
+
const file = queue.shift() as string
|
|
91
|
+
if (seen.has(file)) continue
|
|
92
|
+
seen.add(file)
|
|
93
|
+
const text = await distText(file)
|
|
94
|
+
total += text
|
|
95
|
+
for (const match of text.matchAll(/(?:from|import)\s*"(\.[^"]+\.js)"/g)) {
|
|
96
|
+
const target = new URL(match[1] as string, `file:///${file}`).pathname.slice(1)
|
|
97
|
+
queue.push(target)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return total
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it('não puxa o xyflow nem o ConversationsWorkspace', async () => {
|
|
104
|
+
const bundle = await participantBundle()
|
|
105
|
+
|
|
106
|
+
expect(bundle).not.toContain('xyflow')
|
|
107
|
+
expect(bundle).not.toContain('ConversationsWorkspace')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('fica abaixo de 250 KB com os chunks que importa', async () => {
|
|
111
|
+
const bundle = await participantBundle()
|
|
112
|
+
|
|
113
|
+
expect(new TextEncoder().encode(bundle).length).toBeLessThan(PARTICIPANT_ENTRY_LIMIT_BYTES)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('entrega a tela e os tipos no subpath /participant', async () => {
|
|
117
|
+
expect(await distText('participant/index.js')).toContain('ParticipantConversations')
|
|
118
|
+
expect(await distText('participant/index.d.ts')).toContain('ParticipantConversations')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -45,3 +45,20 @@ describe('linhas da capacidade que a tela não pode fingir', () => {
|
|
|
45
45
|
expect(CORE_CHANNEL_CAPABILITIES.portal.audio).toEqual({ plays: true, records: false })
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
// App e portal são canais do contrato que a UI passou a exibir (T1.0a): a capacidade vem do
|
|
50
|
+
// contrato, nunca da regra do WhatsApp, que daria janela de 24h e 100 MB onde não existem.
|
|
51
|
+
describe('canais app e portal', () => {
|
|
52
|
+
it('app: anexo de 25 MB, grava áudio e não tem janela de sessão', () => {
|
|
53
|
+
const capability = channelCapabilityFor(CONVERSATION_CHANNEL.APP)
|
|
54
|
+
|
|
55
|
+
expect(capability).toBe(CORE_CHANNEL_CAPABILITIES.app)
|
|
56
|
+
expect(capability.attachments.maxBytes).toBe(25 * 1024 * 1024)
|
|
57
|
+
expect(capability.audio.records).toBe(true)
|
|
58
|
+
expect(capability.sessionWindowHours).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('portal: ouve áudio e não grava', () => {
|
|
62
|
+
expect(channelCapabilityFor(CONVERSATION_CHANNEL.PORTAL).audio.records).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { CONVERSATION_CHANNEL
|
|
2
|
+
import { CONVERSATION_CHANNEL as CORE_CONVERSATION_CHANNEL } from '@adatechnology/conversation-contracts'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CHANNEL_CAPABILITIES,
|
|
6
|
+
CHANNEL_FILTER_ALL,
|
|
7
|
+
CONVERSATION_CHANNEL,
|
|
8
|
+
HANDLE_KIND,
|
|
9
|
+
REOPEN_MECHANISM,
|
|
10
|
+
channelFiltersFor,
|
|
11
|
+
type ConversationChannel,
|
|
12
|
+
} from './conversationChannel'
|
|
3
13
|
|
|
4
14
|
describe('channelFiltersFor', () => {
|
|
5
15
|
// O ponto do helper: a barra oferece só o que existe. Oferecer Instagram numa conta que só tem
|
|
@@ -51,3 +61,22 @@ describe('channelFiltersFor', () => {
|
|
|
51
61
|
expect(first).toEqual(second)
|
|
52
62
|
})
|
|
53
63
|
})
|
|
64
|
+
|
|
65
|
+
describe('canais do contrato exibidos pela UI', () => {
|
|
66
|
+
// Canal novo no contrato sem entrada aqui reprova no tsc (Record completo); este teste cobre o
|
|
67
|
+
// mesmo ponto em runtime, para o caso de o vocabulário ser alterado sem recompilar a UI.
|
|
68
|
+
it('todo canal do contrato tem entrada de exibição', () => {
|
|
69
|
+
for (const channel of CORE_CONVERSATION_CHANNEL) {
|
|
70
|
+
expect(CHANNEL_CAPABILITIES[channel as ConversationChannel]).toBeDefined()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('app e portal existem como canais, com rótulo e sem reabertura por template ou tag', () => {
|
|
75
|
+
expect(CONVERSATION_CHANNEL.APP).toBe('app')
|
|
76
|
+
expect(CONVERSATION_CHANNEL.PORTAL).toBe('portal')
|
|
77
|
+
expect(CHANNEL_CAPABILITIES.app.reopenMechanism).toBe(REOPEN_MECHANISM.NONE)
|
|
78
|
+
expect(CHANNEL_CAPABILITIES.portal.reopenMechanism).toBe(REOPEN_MECHANISM.NONE)
|
|
79
|
+
expect(CHANNEL_CAPABILITIES.app.handleKind).toBe(HANDLE_KIND.SESSION)
|
|
80
|
+
expect(CHANNEL_CAPABILITIES.portal.handleKind).toBe(HANDLE_KIND.SESSION)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -10,16 +10,22 @@
|
|
|
10
10
|
* funcionando exatamente como antes.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type { ConversationChannel as CoreConversationChannel } from '@adatechnology/conversation-contracts'
|
|
14
|
+
|
|
13
15
|
import { formatPhone, phoneCountryFlag } from './lib/phone'
|
|
14
16
|
|
|
15
17
|
export const CONVERSATION_CHANNEL = {
|
|
16
18
|
WHATSAPP: 'whatsapp',
|
|
17
19
|
MESSENGER: 'messenger',
|
|
18
20
|
INSTAGRAM: 'instagram',
|
|
21
|
+
APP: 'app',
|
|
22
|
+
PORTAL: 'portal',
|
|
19
23
|
WEBCHAT: 'webchat',
|
|
20
24
|
EMAIL: 'email',
|
|
21
25
|
} as const
|
|
22
|
-
|
|
26
|
+
|
|
27
|
+
/** O que o contrato conhece, mais os dois canais da Meta que a UI já servia antes dele. */
|
|
28
|
+
export type ConversationChannel = CoreConversationChannel | 'messenger' | 'instagram'
|
|
23
29
|
|
|
24
30
|
export const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel = CONVERSATION_CHANNEL.WHATSAPP
|
|
25
31
|
|
|
@@ -75,6 +81,18 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
|
|
|
75
81
|
reopenMechanism: REOPEN_MECHANISM.TAG,
|
|
76
82
|
handleKind: HANDLE_KIND.USERNAME,
|
|
77
83
|
},
|
|
84
|
+
[CONVERSATION_CHANNEL.APP]: {
|
|
85
|
+
label: 'App',
|
|
86
|
+
icon: '📱',
|
|
87
|
+
reopenMechanism: REOPEN_MECHANISM.NONE,
|
|
88
|
+
handleKind: HANDLE_KIND.SESSION,
|
|
89
|
+
},
|
|
90
|
+
[CONVERSATION_CHANNEL.PORTAL]: {
|
|
91
|
+
label: 'Portal',
|
|
92
|
+
icon: '🔗',
|
|
93
|
+
reopenMechanism: REOPEN_MECHANISM.NONE,
|
|
94
|
+
handleKind: HANDLE_KIND.SESSION,
|
|
95
|
+
},
|
|
78
96
|
[CONVERSATION_CHANNEL.WEBCHAT]: {
|
|
79
97
|
// Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
|
|
80
98
|
label: 'Chat do site',
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { ChannelCapability } from '@adatechnology/conversation-contracts'
|
|
4
|
+
|
|
5
|
+
import { channelCapabilityFor } from './channelCapability'
|
|
6
|
+
import { CHANNEL_FILTER_ALL, channelFiltersFor, type ConversationChannel } from './conversationChannel'
|
|
7
|
+
|
|
8
|
+
const MEGABYTE = 1024 * 1024
|
|
9
|
+
|
|
10
|
+
// Values published by conversation-contracts 0.3.0 (b9f1ef6), read by the 0.4.2 UI.
|
|
11
|
+
const LEGACY_WHATSAPP_CAPABILITY: ChannelCapability = {
|
|
12
|
+
confirmsRead: true,
|
|
13
|
+
sessionWindowHours: 24,
|
|
14
|
+
attachments: { accepted: true, maxBytes: 100 * MEGABYTE, maxTotalBytes: null },
|
|
15
|
+
audio: { plays: true, records: true },
|
|
16
|
+
quickReplies: true,
|
|
17
|
+
requiresTransport: false,
|
|
18
|
+
reachableStatuses: ['queued', 'sent', 'delivered', 'read', 'failed'],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LEGACY_EMAIL_CAPABILITY: ChannelCapability = {
|
|
22
|
+
confirmsRead: false,
|
|
23
|
+
sessionWindowHours: null,
|
|
24
|
+
attachments: { accepted: true, maxBytes: 10 * MEGABYTE, maxTotalBytes: 25 * MEGABYTE },
|
|
25
|
+
audio: { plays: true, records: true },
|
|
26
|
+
quickReplies: true,
|
|
27
|
+
requiresTransport: true,
|
|
28
|
+
reachableStatuses: ['queued', 'sent', 'delivered', 'failed', 'bounced'],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LEGACY_WEBCHAT_CAPABILITY: ChannelCapability = {
|
|
32
|
+
confirmsRead: false,
|
|
33
|
+
sessionWindowHours: null,
|
|
34
|
+
attachments: { accepted: true, maxBytes: 10 * MEGABYTE, maxTotalBytes: null },
|
|
35
|
+
audio: { plays: false, records: false },
|
|
36
|
+
quickReplies: true,
|
|
37
|
+
requiresTransport: false,
|
|
38
|
+
reachableStatuses: ['queued', 'delivered', 'failed'],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('channelCapabilityFor mantém os valores da 0.4.2', () => {
|
|
42
|
+
it('G9: whatsapp', () => {
|
|
43
|
+
expect(channelCapabilityFor('whatsapp')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('G9: email', () => {
|
|
47
|
+
expect(channelCapabilityFor('email')).toEqual(LEGACY_EMAIL_CAPABILITY)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('G9: webchat', () => {
|
|
51
|
+
expect(channelCapabilityFor('webchat')).toEqual(LEGACY_WEBCHAT_CAPABILITY)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('G9: messenger cai na capacidade do WhatsApp', () => {
|
|
55
|
+
expect(channelCapabilityFor('messenger')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('G9: instagram cai na capacidade do WhatsApp', () => {
|
|
59
|
+
expect(channelCapabilityFor('instagram')).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('G9: canal ausente cai na capacidade do WhatsApp', () => {
|
|
63
|
+
expect(channelCapabilityFor(undefined)).toEqual(LEGACY_WHATSAPP_CAPABILITY)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('channelFiltersFor com canais antigos mantém lista e ordem da 0.4.2', () => {
|
|
68
|
+
it('G9: os cinco canais antigos saem na ordem do catálogo, não na de chegada', () => {
|
|
69
|
+
const channels: ConversationChannel[] = ['email', 'webchat', 'instagram', 'messenger', 'whatsapp']
|
|
70
|
+
|
|
71
|
+
expect(channelFiltersFor(channels.map((channel) => ({ channel })))).toEqual([
|
|
72
|
+
{ value: CHANNEL_FILTER_ALL, label: 'Todos' },
|
|
73
|
+
{ value: 'whatsapp', label: 'WhatsApp' },
|
|
74
|
+
{ value: 'messenger', label: 'Messenger' },
|
|
75
|
+
{ value: 'instagram', label: 'Instagram' },
|
|
76
|
+
{ value: 'webchat', label: 'Chat do site' },
|
|
77
|
+
{ value: 'email', label: 'E-mail' },
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('G9: conversa sem canal conta como WhatsApp e um canal só não gera filtro', () => {
|
|
82
|
+
expect(channelFiltersFor([{}, { channel: 'whatsapp' }])).toEqual([])
|
|
83
|
+
expect(channelFiltersFor([{}, { channel: 'email' }])).toEqual([
|
|
84
|
+
{ value: CHANNEL_FILTER_ALL, label: 'Todos' },
|
|
85
|
+
{ value: 'whatsapp', label: 'WhatsApp' },
|
|
86
|
+
{ value: 'email', label: 'E-mail' },
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { MessageBubble } from './MessageBubble'
|
|
5
|
+
import { MessageText } from './MessageText'
|
|
6
|
+
import { MessageTextCopiedBadge } from './MessageTextCopiedBadge'
|
|
7
|
+
import { StatusTicks } from './StatusTicks'
|
|
8
|
+
import type { MessagePayload } from './types'
|
|
9
|
+
|
|
10
|
+
// Literal markup rendered by @adatechnology/conversations-ui 0.4.2 (b9f1ef6): the default appearance of
|
|
11
|
+
// these shared components must never drift, because hosts upgrade without touching their markup.
|
|
12
|
+
const LEGACY_TEXT_MARKUP =
|
|
13
|
+
'<div class="text-[14.2px] leading-[19px] whitespace-pre-wrap break-words select-all [&_strong]:font-bold [&_em]:italic [&_del]:line-through"><div><strong>oi</strong></div></div>'
|
|
14
|
+
|
|
15
|
+
const LEGACY_TEMPLATE_MARKUP = '<p class="text-sm italic text-[#667781] dark:text-[#8696a0]">Olá modelo</p>'
|
|
16
|
+
|
|
17
|
+
const LEGACY_TICKS_SENT =
|
|
18
|
+
'<span class="cursor-help leading-none flex items-center text-black/40 dark:text-white/40"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L5 10.5L14.5 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>'
|
|
19
|
+
|
|
20
|
+
const LEGACY_TICKS_DELIVERED =
|
|
21
|
+
'<span class="cursor-help leading-none flex items-center text-black/40 dark:text-white/40"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>'
|
|
22
|
+
|
|
23
|
+
const LEGACY_TICKS_READ =
|
|
24
|
+
'<span class="cursor-help leading-none flex items-center text-sky-500"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>'
|
|
25
|
+
|
|
26
|
+
const LEGACY_TICKS_FAILED =
|
|
27
|
+
'<span class="cursor-help leading-none flex items-center text-red-500"><svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert lucide-alert-triangle" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span>'
|
|
28
|
+
|
|
29
|
+
const LEGACY_TICKS_READ_WITH_TITLE =
|
|
30
|
+
'<span class="cursor-help leading-none flex items-center text-sky-500" data-cv-tooltip="x"><svg viewBox="0 0 20 12" width="15" height="9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 6.5L4.5 10L11 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 6.5L9.5 10L19 1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>'
|
|
31
|
+
|
|
32
|
+
const LEGACY_COPIED_BADGE_MARKUP =
|
|
33
|
+
'<span class="absolute top-0 right-0 -translate-y-full bg-[#3b4a54] text-white text-[11px] px-1.5 py-0.5 rounded shadow-lg">Copiado!</span>'
|
|
34
|
+
|
|
35
|
+
const LEGACY_UNKNOWN_STATUS_MARKUP = LEGACY_TICKS_DELIVERED
|
|
36
|
+
|
|
37
|
+
function buildMessage(type: MessagePayload['type'], content: string): MessagePayload {
|
|
38
|
+
return {
|
|
39
|
+
id: 'msg-1',
|
|
40
|
+
type,
|
|
41
|
+
direction: 'inbound',
|
|
42
|
+
sender: 'customer',
|
|
43
|
+
timestamp: '2026-08-05T12:00:00.000Z',
|
|
44
|
+
content,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('MessageText mantém o markup da 0.4.2 por padrão', () => {
|
|
49
|
+
it('G1: texto comum é um div clicável sem role, tabindex nem teclado', () => {
|
|
50
|
+
const markup = renderToStaticMarkup(<MessageText message={buildMessage('text', '*oi*')} />)
|
|
51
|
+
|
|
52
|
+
expect(markup).toBe(LEGACY_TEXT_MARKUP)
|
|
53
|
+
expect(markup).not.toContain('role=')
|
|
54
|
+
expect(markup).not.toContain('tabindex')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('G2: template continua sendo o parágrafo em itálico', () => {
|
|
58
|
+
const markup = renderToStaticMarkup(<MessageText message={buildMessage('template', 'Olá modelo')} />)
|
|
59
|
+
|
|
60
|
+
expect(markup).toBe(LEGACY_TEMPLATE_MARKUP)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('G3: o selo de cópia é "Copiado!" com a classe Tailwind exata', () => {
|
|
64
|
+
expect(renderToStaticMarkup(<MessageTextCopiedBadge />)).toBe(LEGACY_COPIED_BADGE_MARKUP)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('StatusTicks mantém o markup da 0.4.2 por padrão', () => {
|
|
69
|
+
it('G4: sent', () => {
|
|
70
|
+
expect(renderToStaticMarkup(<StatusTicks status="sent" />)).toBe(LEGACY_TICKS_SENT)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('G4: delivered', () => {
|
|
74
|
+
expect(renderToStaticMarkup(<StatusTicks status="delivered" />)).toBe(LEGACY_TICKS_DELIVERED)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('G4: read', () => {
|
|
78
|
+
expect(renderToStaticMarkup(<StatusTicks status="read" />)).toBe(LEGACY_TICKS_READ)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('G4: failed', () => {
|
|
82
|
+
expect(renderToStaticMarkup(<StatusTicks status="failed" />)).toBe(LEGACY_TICKS_FAILED)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('G5: status desconhecido desenha dois tiques cinza', () => {
|
|
86
|
+
expect(renderToStaticMarkup(<StatusTicks status="pending" />)).toBe(LEGACY_UNKNOWN_STATUS_MARKUP)
|
|
87
|
+
expect(renderToStaticMarkup(<StatusTicks status="xyz" />)).toBe(LEGACY_UNKNOWN_STATUS_MARKUP)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('G5: queued e bounced, que a 0.4.2 nunca recebia, também caem nos dois tiques cinza', () => {
|
|
91
|
+
expect(renderToStaticMarkup(<StatusTicks status="queued" />)).toBe(LEGACY_UNKNOWN_STATUS_MARKUP)
|
|
92
|
+
expect(renderToStaticMarkup(<StatusTicks status="bounced" />)).toBe(LEGACY_UNKNOWN_STATUS_MARKUP)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('G6: title vira data-cv-tooltip e nunca aria-label', () => {
|
|
96
|
+
const markup = renderToStaticMarkup(<StatusTicks status="read" title="x" />)
|
|
97
|
+
|
|
98
|
+
expect(markup).toBe(LEGACY_TICKS_READ_WITH_TITLE)
|
|
99
|
+
expect(markup).toContain('data-cv-tooltip="x"')
|
|
100
|
+
expect(markup).not.toContain('aria-label')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('MessageBubble mantém o selo da 0.4.2', () => {
|
|
105
|
+
it('G7: mensagem enviada e lida contém o StatusTicks idêntico ao da 0.4.2', () => {
|
|
106
|
+
const message: MessagePayload = {
|
|
107
|
+
...buildMessage('text', 'oi'),
|
|
108
|
+
direction: 'outbound',
|
|
109
|
+
sender: 'agent',
|
|
110
|
+
status: 'read',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expect(renderToStaticMarkup(<MessageBubble message={message} isMine />)).toContain(LEGACY_TICKS_READ)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('G7: bounced no balão não vira falha: sem anel de erro e com dois tiques cinza', () => {
|
|
117
|
+
const message: MessagePayload = {
|
|
118
|
+
...buildMessage('text', 'oi'),
|
|
119
|
+
direction: 'outbound',
|
|
120
|
+
sender: 'agent',
|
|
121
|
+
status: 'bounced',
|
|
122
|
+
}
|
|
123
|
+
const markup = renderToStaticMarkup(<MessageBubble message={message} isMine />)
|
|
124
|
+
|
|
125
|
+
expect(markup).toContain(LEGACY_TICKS_DELIVERED)
|
|
126
|
+
})
|
|
127
|
+
})
|