@adatechnology/conversations-ui 0.1.1 → 0.2.1
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/dist/{ConversationSimulatorPanel--5fIzXWY.d.ts → ConversationSimulatorPanel-ikgmlciE.d.ts} +103 -1
- package/dist/{chunk-BJNRLLDO.js → chunk-NHQDQJL2.js} +769 -244
- package/dist/index.d.ts +304 -9
- package/dist/index.js +2181 -507
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +147 -1
- package/dist/styles.css +135 -0
- package/package.json +1 -1
- package/src/MessageComposer.test.tsx +14 -0
- package/src/MessageComposer.tsx +327 -73
- package/src/RichMessageComposer.test.tsx +22 -4
- package/src/RichMessageComposer.tsx +674 -370
- package/src/index.ts +28 -1
- package/src/preview/createMockConversationsApi.ts +184 -0
- package/src/providers/types.ts +34 -0
- package/src/quickReplies/AttachmentsFormSection.tsx +160 -0
- package/src/quickReplies/QuickRepliesPicker.test.tsx +114 -0
- package/src/quickReplies/QuickRepliesPicker.tsx +188 -0
- package/src/quickReplies/QuickRepliesWorkspace.test.tsx +32 -0
- package/src/quickReplies/QuickRepliesWorkspace.tsx +395 -0
- package/src/quickReplies/createUploadQueue.test.ts +106 -0
- package/src/quickReplies/createUploadQueue.ts +69 -0
- package/src/quickReplies/index.ts +5 -0
- package/src/quickReplies/labels.ts +124 -0
- package/src/quickReplies/quickReply.types.ts +74 -0
- package/src/quickReplies/quickReplyAttachmentUpload.test.ts +76 -0
- package/src/quickReplies/quickReplyAttachmentUpload.ts +81 -0
- package/src/quickReplies/quickReplyAttachments.test.ts +467 -0
- package/src/quickReplies/quickReplyAttachments.ts +328 -0
- package/src/quickReplies/quickReplySearch.test.ts +88 -0
- package/src/quickReplies/quickReplySearch.ts +104 -0
- package/src/quickReplies/quickReplyShortcut.test.ts +38 -0
- package/src/quickReplies/quickReplyShortcut.ts +46 -0
- package/src/quickReplies/resolveConversationVariables.test.ts +63 -0
- package/src/quickReplies/resolveConversationVariables.ts +41 -0
- package/src/quickReplies/useQuickRepliesPicker.test.ts +20 -0
- package/src/quickReplies/useQuickRepliesPicker.ts +121 -0
- package/src/quickReplies/useQuickRepliesWorkspace.test.ts +222 -0
- package/src/quickReplies/useQuickRepliesWorkspace.ts +426 -0
- package/src/quickReplies/useQuickReplyAttachmentUploads.ts +159 -0
- package/src/styles.css +87 -0
- package/src/workspace/ConversationPane.tsx +137 -73
- package/src/workspace/ConversationsWorkspace.tsx +16 -1
- package/src/workspace/QueuedAttachmentsList.test.ts +27 -0
- package/src/workspace/QueuedAttachmentsList.tsx +198 -0
- package/src/workspace/index.ts +1 -0
- package/src/workspace/labels.ts +14 -0
- package/src/workspace/useComposerAttachmentRetry.ts +169 -0
- package/src/workspace/useComposerQueue.ts +229 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RichComposerVariable } from '../RichMessageComposer'
|
|
2
|
+
import type { ConversationVariable } from './quickReply.types'
|
|
3
|
+
|
|
4
|
+
export type ResolveConversationVariablesParams = {
|
|
5
|
+
readonly conversationVariables?: readonly ConversationVariable[] | undefined
|
|
6
|
+
/** @deprecated Use `conversationVariables`. */
|
|
7
|
+
readonly quickReplyVariables?: Readonly<Record<string, string>> | undefined
|
|
8
|
+
/** @deprecated Use `conversationVariables`. */
|
|
9
|
+
readonly composerVariables?: readonly RichComposerVariable[] | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ResolveConversationVariablesResult = {
|
|
13
|
+
readonly quickReplyVariables: Readonly<Record<string, string>> | undefined
|
|
14
|
+
readonly composerVariables: readonly RichComposerVariable[] | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MARKER_PATTERN = /^\{\{\s*([\w.]+)\s*\}\}$/
|
|
18
|
+
|
|
19
|
+
/** Nome que `applyQuickReplyVariables` casa; marcador fora do formato cai no `id`. */
|
|
20
|
+
function variableNameOf(variable: ConversationVariable): string {
|
|
21
|
+
return MARKER_PATTERN.exec(variable.marker)?.[1] ?? variable.id
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Deriva os dois formatos antigos da lista nova. Quando a lista vem, ela manda — misturar as fontes
|
|
26
|
+
* faria o chip e o botão de variáveis mostrarem valores diferentes. Sem ela, as props antigas seguem
|
|
27
|
+
* valendo para os produtos que ainda não migraram.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveConversationVariables({
|
|
30
|
+
conversationVariables,
|
|
31
|
+
quickReplyVariables,
|
|
32
|
+
composerVariables,
|
|
33
|
+
}: ResolveConversationVariablesParams): ResolveConversationVariablesResult {
|
|
34
|
+
if (!conversationVariables) return { quickReplyVariables, composerVariables }
|
|
35
|
+
// Valor vazio não é oferecido: inserir o nada no texto só esconde que o dado falta.
|
|
36
|
+
const known = conversationVariables.filter((variable) => variable.value !== '')
|
|
37
|
+
return {
|
|
38
|
+
quickReplyVariables: Object.fromEntries(known.map((variable) => [variableNameOf(variable), variable.value])),
|
|
39
|
+
composerVariables: known.map(({ id, label, value }) => ({ id, label, value })),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { nextHighlightedIndex } from './useQuickRepliesPicker'
|
|
4
|
+
|
|
5
|
+
describe('nextHighlightedIndex', () => {
|
|
6
|
+
it('avança e roda para o começo no fim da lista (ArrowDown)', () => {
|
|
7
|
+
expect(nextHighlightedIndex(0, 3, 1)).toBe(1)
|
|
8
|
+
expect(nextHighlightedIndex(2, 3, 1)).toBe(0)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('volta e roda para o fim no começo da lista (ArrowUp)', () => {
|
|
12
|
+
expect(nextHighlightedIndex(1, 3, -1)).toBe(0)
|
|
13
|
+
expect(nextHighlightedIndex(0, 3, -1)).toBe(2)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('lista vazia sempre fica em zero', () => {
|
|
17
|
+
expect(nextHighlightedIndex(0, 0, 1)).toBe(0)
|
|
18
|
+
expect(nextHighlightedIndex(0, 0, -1)).toBe(0)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { filterQuickReplies } from './quickReplySearch'
|
|
3
|
+
import type { QuickReply } from './quickReply.types'
|
|
4
|
+
|
|
5
|
+
export type UseQuickRepliesPickerParams = {
|
|
6
|
+
readonly isOpen: boolean
|
|
7
|
+
readonly search: string
|
|
8
|
+
readonly listQuickReplies?: (params?: { search?: string }) => Promise<QuickReply[]>
|
|
9
|
+
readonly quickReplyVariables?: Readonly<Record<string, string>>
|
|
10
|
+
readonly onSelect: (quickReply: QuickReply) => void
|
|
11
|
+
readonly onClose: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Extraída para ser testável sem montar o hook: a aritmética do círculo é o que a tecla decide. */
|
|
15
|
+
export function nextHighlightedIndex(current: number, length: number, delta: 1 | -1): number {
|
|
16
|
+
if (length === 0) return 0
|
|
17
|
+
return (current + delta + length) % length
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type UseQuickRepliesPickerResult = {
|
|
21
|
+
readonly items: readonly QuickReply[]
|
|
22
|
+
readonly isLoading: boolean
|
|
23
|
+
/** Sinalizador puro — a mensagem exibida é sempre a do rótulo do host, nunca `caught.message` cru. */
|
|
24
|
+
readonly hasError: boolean
|
|
25
|
+
readonly highlightedIndex: number
|
|
26
|
+
readonly setHighlightedIndex: (index: number) => void
|
|
27
|
+
readonly handleKeyDown: (event: { key: string; preventDefault: () => void }) => void
|
|
28
|
+
/** Refaz a busca ignorando o cache — usado quando o host quer forçar atualização da lista. */
|
|
29
|
+
readonly refetch: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Estado do picker do atalho `/` e do botão de raio. A lista é da instalação inteira, não por
|
|
34
|
+
* conversa — carregada uma vez por instância do hook (`cacheRef`, sobrevive a fechar/abrir o
|
|
35
|
+
* picker) e nunca reconsultada só porque a conversa mudou. `requestIdRef` descarta resposta de
|
|
36
|
+
* pedido antigo: trocar de conversa ou reabrir rápido não pode deixar `isLoading` preso em `true`
|
|
37
|
+
* nem aplicar um resultado que não é mais o mais recente.
|
|
38
|
+
*/
|
|
39
|
+
export function useQuickRepliesPicker({
|
|
40
|
+
isOpen,
|
|
41
|
+
search,
|
|
42
|
+
listQuickReplies,
|
|
43
|
+
quickReplyVariables,
|
|
44
|
+
onSelect,
|
|
45
|
+
onClose,
|
|
46
|
+
}: UseQuickRepliesPickerParams): UseQuickRepliesPickerResult {
|
|
47
|
+
const cacheRef = useRef<QuickReply[] | undefined>(undefined)
|
|
48
|
+
const requestIdRef = useRef(0)
|
|
49
|
+
const [items, setItems] = useState<QuickReply[]>([])
|
|
50
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
51
|
+
const [hasError, setHasError] = useState(false)
|
|
52
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
|
53
|
+
|
|
54
|
+
const load = useCallback(() => {
|
|
55
|
+
if (!listQuickReplies) return
|
|
56
|
+
const requestId = ++requestIdRef.current
|
|
57
|
+
setIsLoading(true)
|
|
58
|
+
setHasError(false)
|
|
59
|
+
listQuickReplies()
|
|
60
|
+
.then((result) => {
|
|
61
|
+
if (requestIdRef.current !== requestId) return
|
|
62
|
+
cacheRef.current = result
|
|
63
|
+
setItems(result)
|
|
64
|
+
setIsLoading(false)
|
|
65
|
+
})
|
|
66
|
+
.catch(() => {
|
|
67
|
+
if (requestIdRef.current !== requestId) return
|
|
68
|
+
setHasError(true)
|
|
69
|
+
setIsLoading(false)
|
|
70
|
+
})
|
|
71
|
+
}, [listQuickReplies])
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!isOpen || !listQuickReplies) return
|
|
75
|
+
if (cacheRef.current) {
|
|
76
|
+
setItems(cacheRef.current)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
load()
|
|
80
|
+
}, [isOpen, listQuickReplies, load])
|
|
81
|
+
|
|
82
|
+
const refetch = useCallback(() => {
|
|
83
|
+
cacheRef.current = undefined
|
|
84
|
+
load()
|
|
85
|
+
}, [load])
|
|
86
|
+
|
|
87
|
+
const filtered = filterQuickReplies({ quickReplies: items, search, variables: quickReplyVariables })
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setHighlightedIndex(0)
|
|
91
|
+
}, [search, filtered.length])
|
|
92
|
+
|
|
93
|
+
const handleKeyDown = useCallback(
|
|
94
|
+
(event: { key: string; preventDefault: () => void }) => {
|
|
95
|
+
if (event.key === 'ArrowDown') {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
setHighlightedIndex((index) => nextHighlightedIndex(index, filtered.length, 1))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (event.key === 'ArrowUp') {
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
setHighlightedIndex((index) => nextHighlightedIndex(index, filtered.length, -1))
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (event.key === 'Enter') {
|
|
106
|
+
const selected = filtered[highlightedIndex]
|
|
107
|
+
if (!selected) return
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
onSelect(selected)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (event.key === 'Escape') {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
onClose()
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
[filtered, highlightedIndex, onSelect, onClose],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return { items: filtered, isLoading, hasError, highlightedIndex, setHighlightedIndex, handleKeyDown, refetch }
|
|
121
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
insertAtCursor,
|
|
5
|
+
submitQuickReply,
|
|
6
|
+
validateQuickReplyInput,
|
|
7
|
+
type QuickRepliesWorkspaceApi,
|
|
8
|
+
type QuickRepliesWorkspaceEditing,
|
|
9
|
+
} from './useQuickRepliesWorkspace'
|
|
10
|
+
import { DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS } from './labels'
|
|
11
|
+
import type { QuickReply } from './quickReply.types'
|
|
12
|
+
|
|
13
|
+
const LABELS = DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS
|
|
14
|
+
|
|
15
|
+
const VALID_EDITING: QuickRepliesWorkspaceEditing = {
|
|
16
|
+
id: null,
|
|
17
|
+
title: 'Saudação',
|
|
18
|
+
shortcut: 'ola',
|
|
19
|
+
body: 'Olá!',
|
|
20
|
+
attachments: [],
|
|
21
|
+
}
|
|
22
|
+
const SAVED: QuickReply = { id: '1', title: 'Saudação', shortcut: 'ola', body: 'Olá!' }
|
|
23
|
+
|
|
24
|
+
describe('validateQuickReplyInput', () => {
|
|
25
|
+
it('aceita título, atalho e corpo dentro do limite', () => {
|
|
26
|
+
expect(validateQuickReplyInput({ title: 'Saudação', shortcut: 'ola', body: 'Olá!' }, LABELS)).toEqual({})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejeita título vazio ou maior que 40 caracteres', () => {
|
|
30
|
+
expect(validateQuickReplyInput({ title: '', shortcut: 'ola', body: 'Olá!' }, LABELS).title).toBeDefined()
|
|
31
|
+
expect(
|
|
32
|
+
validateQuickReplyInput({ title: 'x'.repeat(41), shortcut: 'ola', body: 'Olá!' }, LABELS).title,
|
|
33
|
+
).toBeDefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejeita atalho com maiúscula, espaço ou acento', () => {
|
|
37
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'Ola', body: 'B' }, LABELS).shortcut).toBeDefined()
|
|
38
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'ol a', body: 'B' }, LABELS).shortcut).toBeDefined()
|
|
39
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'olá', body: 'B' }, LABELS).shortcut).toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('aceita atalho com hífen e número', () => {
|
|
43
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'doc-2', body: 'B' }, LABELS).shortcut).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('rejeita corpo vazio ou maior que 1000 caracteres', () => {
|
|
47
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'ola', body: '' }, LABELS).body).toBeDefined()
|
|
48
|
+
expect(validateQuickReplyInput({ title: 'T', shortcut: 'ola', body: 'x'.repeat(1001) }, LABELS).body).toBeDefined()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('submitQuickReply', () => {
|
|
53
|
+
it('cria quando id é null e chama createQuickReply', async () => {
|
|
54
|
+
const api: QuickRepliesWorkspaceApi = { createQuickReply: async () => SAVED }
|
|
55
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
56
|
+
expect(result).toEqual({ outcome: 'saved', quickReply: SAVED })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('atualiza quando há id e chama updateQuickReply com ele', async () => {
|
|
60
|
+
let calledWith: string | undefined
|
|
61
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
62
|
+
updateQuickReply: async (id) => {
|
|
63
|
+
calledWith = id
|
|
64
|
+
return SAVED
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
const result = await submitQuickReply({ api, editing: { ...VALID_EDITING, id: '1' }, labels: LABELS })
|
|
68
|
+
expect(result).toEqual({ outcome: 'saved', quickReply: SAVED })
|
|
69
|
+
expect(calledWith).toBe('1')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('manda attachmentUploadIds só quando o host tem a porta de upload (QR-30)', async () => {
|
|
73
|
+
let receivedInput: unknown
|
|
74
|
+
const withUpload: QuickRepliesWorkspaceApi = {
|
|
75
|
+
createQuickReply: async (input) => {
|
|
76
|
+
receivedInput = input
|
|
77
|
+
return SAVED
|
|
78
|
+
},
|
|
79
|
+
uploadQuickReplyAttachment: async () => ({ uploadId: 'x', filename: 'x', mimeType: 'x', sizeBytes: 1 }),
|
|
80
|
+
}
|
|
81
|
+
const editingWithAttachments = {
|
|
82
|
+
...VALID_EDITING,
|
|
83
|
+
attachments: [{ uploadId: 'a', filename: 'a.pdf', mimeType: 'application/pdf', sizeBytes: 1 }],
|
|
84
|
+
}
|
|
85
|
+
await submitQuickReply({ api: withUpload, editing: editingWithAttachments, labels: LABELS })
|
|
86
|
+
expect((receivedInput as { attachmentUploadIds?: readonly string[] }).attachmentUploadIds).toEqual(['a'])
|
|
87
|
+
|
|
88
|
+
const withoutUpload: QuickRepliesWorkspaceApi = {
|
|
89
|
+
createQuickReply: async (input) => {
|
|
90
|
+
receivedInput = input
|
|
91
|
+
return SAVED
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
await submitQuickReply({ api: withoutUpload, editing: editingWithAttachments, labels: LABELS })
|
|
95
|
+
expect((receivedInput as { attachmentUploadIds?: readonly string[] }).attachmentUploadIds).toBeUndefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('não chama a api quando a validação falha', async () => {
|
|
99
|
+
let called = false
|
|
100
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
101
|
+
createQuickReply: async () => {
|
|
102
|
+
called = true
|
|
103
|
+
return SAVED
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
const result = await submitQuickReply({ api, editing: { ...VALID_EDITING, title: '' }, labels: LABELS })
|
|
107
|
+
expect(result.outcome).toBe('invalid')
|
|
108
|
+
expect(called).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('409 com code QUICK_REPLY_SHORTCUT_TAKEN vira erro no campo atalho', async () => {
|
|
112
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
113
|
+
createQuickReply: async () => {
|
|
114
|
+
throw { code: 'QUICK_REPLY_SHORTCUT_TAKEN' }
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
118
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: { shortcut: LABELS.shortcutTaken } })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('error.details[] com vários campos aponta cada um no seu campo', async () => {
|
|
122
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
123
|
+
createQuickReply: async () => {
|
|
124
|
+
throw {
|
|
125
|
+
details: [
|
|
126
|
+
{ field: 'title', message: 'Título repetido' },
|
|
127
|
+
{ field: 'shortcut', message: 'Atalho em uso' },
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
outcome: 'rejected',
|
|
135
|
+
fieldErrors: { title: 'Título repetido', shortcut: 'Atalho em uso' },
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('erro sem details vira o rótulo do formulário, nunca a mensagem crua da exceção', async () => {
|
|
140
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
141
|
+
createQuickReply: async () => {
|
|
142
|
+
throw new Error('ECONNRESET: socket hang up')
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
146
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: {}, formError: LABELS.saveError })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('409 com code aninhado em error.response.data.error (axios) vira erro no atalho', async () => {
|
|
150
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
151
|
+
createQuickReply: async () => {
|
|
152
|
+
throw {
|
|
153
|
+
response: {
|
|
154
|
+
data: {
|
|
155
|
+
error: {
|
|
156
|
+
code: 'QUICK_REPLY_SHORTCUT_TAKEN',
|
|
157
|
+
message: 'Atalho já existe',
|
|
158
|
+
details: [{ field: 'shortcut', message: 'Atalho já existe' }],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
166
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: { shortcut: 'Atalho já existe' } })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('erro aninhado em error.error (envelope { error: {...} }) vira erro no atalho', async () => {
|
|
170
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
171
|
+
createQuickReply: async () => {
|
|
172
|
+
throw {
|
|
173
|
+
error: {
|
|
174
|
+
code: 'QUICK_REPLY_SHORTCUT_TAKEN',
|
|
175
|
+
message: 'Atalho já existe',
|
|
176
|
+
details: [{ field: 'shortcut', message: 'Atalho já existe' }],
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
182
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: { shortcut: 'Atalho já existe' } })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('erro aninhado em error.body.error vira erro no atalho', async () => {
|
|
186
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
187
|
+
createQuickReply: async () => {
|
|
188
|
+
throw { body: { error: { code: 'QUICK_REPLY_SHORTCUT_TAKEN' } } }
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
192
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: { shortcut: LABELS.shortcutTaken } })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('erro que se referencia (error.error === error) não trava em recursão infinita', async () => {
|
|
196
|
+
const circular: Record<string, unknown> = {}
|
|
197
|
+
circular.error = circular
|
|
198
|
+
const api: QuickRepliesWorkspaceApi = {
|
|
199
|
+
createQuickReply: async () => {
|
|
200
|
+
throw circular
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
const result = await submitQuickReply({ api, editing: VALID_EDITING, labels: LABELS })
|
|
204
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: {}, formError: LABELS.saveError })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('editando sem updateQuickReply na porta, rejeita com saveUnavailable em vez de fingir sucesso', async () => {
|
|
208
|
+
const api: QuickRepliesWorkspaceApi = {}
|
|
209
|
+
const result = await submitQuickReply({ api, editing: { ...VALID_EDITING, id: '1' }, labels: LABELS })
|
|
210
|
+
expect(result).toEqual({ outcome: 'rejected', fieldErrors: {}, formError: LABELS.saveUnavailable })
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('insertAtCursor', () => {
|
|
215
|
+
it('insere o marcador na posição do cursor', () => {
|
|
216
|
+
expect(insertAtCursor('Olá !', 4, 4, '{{nome}}')).toEqual({ text: 'Olá {{nome}}!', caret: 12 })
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('troca a seleção pelo marcador', () => {
|
|
220
|
+
expect(insertAtCursor('Olá NOME!', 4, 8, '{{nome}}')).toEqual({ text: 'Olá {{nome}}!', caret: 12 })
|
|
221
|
+
})
|
|
222
|
+
})
|