mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-25 22:09:36 +00:00
Bug fixes in live chat widget form and more e2e tests
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
cmodels "github.com/abhinavxd/libredesk/internal/custom_attribute/models"
|
||||
"github.com/abhinavxd/libredesk/internal/envelope"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/zerodha/fastglue"
|
||||
)
|
||||
@@ -112,6 +114,9 @@ func handleDeleteCustomAttribute(r *fastglue.Request) error {
|
||||
if err = app.customAttribute.Delete(id); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
if err := removeAttributeFromPreChatForms(app, id); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope(true)
|
||||
}
|
||||
|
||||
@@ -137,3 +142,88 @@ func validateCustomAttribute(app *App, attribute cmodels.CustomAttribute) error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAttributeFromPreChatForms(app *App, attributeID int) error {
|
||||
inboxes, err := app.inbox.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, inb := range inboxes {
|
||||
if inb.Channel != livechat.ChannelLiveChat {
|
||||
continue
|
||||
}
|
||||
config, changed, err := stripPreChatFormAttribute(inb.Config, attributeID)
|
||||
if err != nil {
|
||||
app.lo.Error("error parsing live chat config", "id", inb.ID, "error", err)
|
||||
return envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
if err := app.inbox.UpdateConfig(inb.ID, config); err != nil {
|
||||
return envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
}
|
||||
if err := reloadInbox(app, inb.ID); err != nil {
|
||||
app.lo.Error("error reloading inbox", "id", inb.ID, "error", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stripPreChatFormAttribute(raw json.RawMessage, attributeID int) (json.RawMessage, bool, error) {
|
||||
var config map[string]any
|
||||
if len(raw) == 0 {
|
||||
return raw, false, nil
|
||||
}
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return raw, false, err
|
||||
}
|
||||
form, ok := config["prechat_form"].(map[string]any)
|
||||
if !ok {
|
||||
return raw, false, nil
|
||||
}
|
||||
var changed bool
|
||||
for _, holder := range []map[string]any{form, asObject(form["visitors"]), asObject(form["users"])} {
|
||||
if holder == nil {
|
||||
continue
|
||||
}
|
||||
fields, ok := holder["fields"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
kept := make([]any, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if attributeIDOf(field) == attributeID {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, field)
|
||||
}
|
||||
holder["fields"] = kept
|
||||
}
|
||||
if !changed {
|
||||
return raw, false, nil
|
||||
}
|
||||
updated, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return raw, false, err
|
||||
}
|
||||
return updated, true, nil
|
||||
}
|
||||
|
||||
func asObject(value any) map[string]any {
|
||||
object, _ := value.(map[string]any)
|
||||
return object
|
||||
}
|
||||
|
||||
func attributeIDOf(field any) int {
|
||||
object, ok := field.(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
id, ok := object["custom_attribute_id"].(float64)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return int(id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStripPreChatFormAttribute(t *testing.T) {
|
||||
raw := json.RawMessage(`{
|
||||
"brand_name":"Acme",
|
||||
"prechat_form":{
|
||||
"fields":[{"key":"name"},{"key":"plan","custom_attribute_id":7}],
|
||||
"visitors":{"fields":[{"key":"plan","custom_attribute_id":7},{"key":"email"}]},
|
||||
"users":{"fields":[{"key":"email"}]}
|
||||
}
|
||||
}`)
|
||||
|
||||
updated, changed, err := stripPreChatFormAttribute(raw, 7)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatal("expected config to change")
|
||||
}
|
||||
|
||||
var config struct {
|
||||
BrandName string `json:"brand_name"`
|
||||
PreChatForm struct {
|
||||
Fields []map[string]any `json:"fields"`
|
||||
Visitors struct {
|
||||
Fields []map[string]any `json:"fields"`
|
||||
} `json:"visitors"`
|
||||
Users struct {
|
||||
Fields []map[string]any `json:"fields"`
|
||||
} `json:"users"`
|
||||
} `json:"prechat_form"`
|
||||
}
|
||||
if err := json.Unmarshal(updated, &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config.BrandName != "Acme" {
|
||||
t.Fatalf("brand name = %q", config.BrandName)
|
||||
}
|
||||
if len(config.PreChatForm.Fields) != 1 || config.PreChatForm.Fields[0]["key"] != "name" {
|
||||
t.Fatalf("fields = %v", config.PreChatForm.Fields)
|
||||
}
|
||||
if len(config.PreChatForm.Visitors.Fields) != 1 || config.PreChatForm.Visitors.Fields[0]["key"] != "email" {
|
||||
t.Fatalf("visitor fields = %v", config.PreChatForm.Visitors.Fields)
|
||||
}
|
||||
if len(config.PreChatForm.Users.Fields) != 1 {
|
||||
t.Fatalf("user fields = %v", config.PreChatForm.Users.Fields)
|
||||
}
|
||||
|
||||
if _, changed, err := stripPreChatFormAttribute(raw, 8); err != nil || changed {
|
||||
t.Fatalf("unused attribute changed = %v, err = %v", changed, err)
|
||||
}
|
||||
if _, changed, err := stripPreChatFormAttribute(json.RawMessage(`{}`), 7); err != nil || changed {
|
||||
t.Fatalf("config without a pre-chat form changed = %v, err = %v", changed, err)
|
||||
}
|
||||
}
|
||||
@@ -1735,7 +1735,7 @@ const submitLabel = computed(() => {
|
||||
})
|
||||
|
||||
const theme = computed(() => form.values.config?.theme || 'light')
|
||||
const editingTheme = ref('light')
|
||||
const editingTheme = inject('livechatPreviewTheme', ref('light'))
|
||||
const brandingPath = computed(() => `config.branding.${editingTheme.value}`)
|
||||
const activeBranding = computed(() => form.values.config?.branding?.[editingTheme.value])
|
||||
const activeBackgroundType = computed(() => activeBranding.value?.home_screen?.background?.type)
|
||||
@@ -1807,13 +1807,12 @@ const updateCampaignCooldown = (duration) =>
|
||||
form.setFieldValue('config.campaign_cooldown', duration, hasCampaignErrors.value)
|
||||
|
||||
// home_apps in form.values only syncs on change events, so pull the live ref for the preview.
|
||||
const previewTheme = inject('livechatPreviewTheme', ref('light'))
|
||||
const previewConfig = computed(() => {
|
||||
const branding = form.values.config?.branding?.[previewTheme.value] || {}
|
||||
const branding = form.values.config?.branding?.[editingTheme.value] || {}
|
||||
return {
|
||||
...form.values.config,
|
||||
...branding,
|
||||
dark: previewTheme.value === 'dark',
|
||||
dark: editingTheme.value === 'dark',
|
||||
launcher: { ...form.values.config?.launcher, ...branding.launcher },
|
||||
home_apps: homeApps.value,
|
||||
help_tree: helpTree.value,
|
||||
|
||||
@@ -28,13 +28,13 @@ export const useHelpStore = defineStore('help', () => {
|
||||
return collect(data.value?.tree)
|
||||
})
|
||||
const available = computed(() => !!data.value && articles.value.length > 0)
|
||||
const featured = computed(() =>
|
||||
data.value?.featured_ids?.length
|
||||
? data.value.featured_ids
|
||||
.map((id) => articles.value.find((article) => article.id === id))
|
||||
.filter(Boolean)
|
||||
: data.value?.popular || []
|
||||
)
|
||||
const featured = computed(() => {
|
||||
const picked = (data.value?.featured_ids || [])
|
||||
.map((id) => articles.value.find((article) => article.id === id))
|
||||
.filter(Boolean)
|
||||
// Featured articles outside the visitor's locale are missing from the tree.
|
||||
return picked.length ? picked : data.value?.popular || []
|
||||
})
|
||||
const reset = () => {
|
||||
generation++
|
||||
identity.value++
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
// The steps run in order and share the inbox created by the first one.
|
||||
|
||||
const stamp = Date.now()
|
||||
const inboxName = `Cypress Branches ${stamp}`
|
||||
const brandName = `Cypress Branches Brand ${stamp}`
|
||||
const emailInboxName = `Cypress Branch Email ${stamp}`
|
||||
const attributeName = `Cypress Branch Attr ${stamp}`
|
||||
const attributeKey = `cypress_branch_attr_${stamp}`
|
||||
const campaignName = `Cypress Branch Campaign ${stamp}`
|
||||
const listPath = '/admin/inboxes'
|
||||
|
||||
const selectOption = (trigger, option) => {
|
||||
cy.get(trigger).click()
|
||||
cy.get('[role="option"]').contains(option).click()
|
||||
// The listbox keeps a focus guard over the form until it has finished closing.
|
||||
cy.get('[role="option"]').should('not.exist')
|
||||
}
|
||||
|
||||
const editInbox = (inboxId) => cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
|
||||
const pickFromLabelledSelect = (label, optionIndex) => {
|
||||
cy.contains('label', label).parent().find('button').click()
|
||||
cy.get('[role="option"]').eq(optionIndex).click()
|
||||
cy.get('[role="option"]').should('not.exist')
|
||||
}
|
||||
|
||||
const linkEmailInbox = (name) => {
|
||||
cy.inboxSection('Conversation continuity email inbox').find('button[role="combobox"]').click()
|
||||
cy.get('[role="option"]').contains(name).click()
|
||||
cy.get('[role="option"]').should('not.exist')
|
||||
}
|
||||
|
||||
// The accordion's open animation makes ':visible' flaky, so these scope by section instead.
|
||||
const audienceEnabledSwitch = () =>
|
||||
cy
|
||||
.inboxSection('Pre-chat form')
|
||||
.contains('p', 'Show form to this audience')
|
||||
.parent()
|
||||
.find('button[role="switch"]')
|
||||
|
||||
const enablePreChatForm = () =>
|
||||
cy.switchField('Enable pre-chat form').then(($switch) => {
|
||||
if ($switch.attr('data-state') !== 'checked') cy.wrap($switch).click()
|
||||
})
|
||||
|
||||
const savedConfig = (alias) => cy.wait(alias).then(({ response }) => response.body.data.config)
|
||||
|
||||
describe('Live chat inbox form: branches the other specs skip', () => {
|
||||
let inboxId
|
||||
let emailInboxId
|
||||
let attributeId
|
||||
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.api('POST', '/api/v1/custom-attributes', {
|
||||
name: attributeName,
|
||||
key: attributeKey,
|
||||
description: 'Cypress branch coverage',
|
||||
applies_to: 'contact',
|
||||
data_type: 'text',
|
||||
values: [],
|
||||
regex: '',
|
||||
regex_hint: ''
|
||||
}).then(({ body }) => {
|
||||
attributeId = body.data.id
|
||||
})
|
||||
cy.api('POST', '/api/v1/inboxes', {
|
||||
name: emailInboxName,
|
||||
channel: 'email',
|
||||
enabled: true,
|
||||
from: `Branch ${stamp} <branch.${stamp}@example.com>`,
|
||||
config: {
|
||||
auth_type: 'password',
|
||||
from: `Branch ${stamp} <branch.${stamp}@example.com>`,
|
||||
smtp: [
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: Number(Cypress.env('SMTP_PORT') || 1025),
|
||||
username: '',
|
||||
password: '',
|
||||
auth_protocol: 'none',
|
||||
tls_type: 'none',
|
||||
max_conns: 2,
|
||||
max_msg_retries: 1,
|
||||
idle_timeout: '5s',
|
||||
pool_wait_timeout: '5s'
|
||||
}
|
||||
],
|
||||
imap: []
|
||||
}
|
||||
}).then(({ body }) => {
|
||||
emailInboxId = body.data.id
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// radix-vue's select focuses a node that is already gone in headless runs.
|
||||
Cypress.on('uncaught:exception', (err) => !err.message.includes("reading 'focus'"))
|
||||
cy.viewport(1280, 900)
|
||||
cy.login()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
if (inboxId) cy.api('DELETE', `/api/v1/inboxes/${inboxId}`, null, { failOnStatusCode: false })
|
||||
if (emailInboxId) {
|
||||
cy.api('DELETE', `/api/v1/inboxes/${emailInboxId}`, null, { failOnStatusCode: false })
|
||||
}
|
||||
if (attributeId) {
|
||||
cy.api('DELETE', `/api/v1/custom-attributes/${attributeId}`, null, {
|
||||
failOnStatusCode: false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('creates the inbox the other steps edit', () => {
|
||||
cy.intercept('POST', '**/api/v1/inboxes').as('createInbox')
|
||||
|
||||
cy.visit(`${listPath}/new`)
|
||||
cy.contains('Create a live chat inbox').click()
|
||||
cy.openInboxSection('General')
|
||||
cy.get('input[name="name"]').type(inboxName)
|
||||
cy.get('input[name="config.brand_name"]').type(brandName)
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.wait('@createInbox').then(({ response }) => {
|
||||
expect(response.statusCode).to.eq(200)
|
||||
inboxId = response.body.data.id
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an image background with no url, then saves one', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
const imageUrl = `https://cypress.test/${stamp}/background.jpg`
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Branding')
|
||||
cy.get('#bg-image').click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Branding').should('have.attr', 'data-state', 'open')
|
||||
|
||||
cy.get('input[name="config.branding.light.home_screen.background.image_url"]').type(imageUrl)
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.branding.light.home_screen.background.type).to.eq('image')
|
||||
expect(config.branding.light.home_screen.background.image_url).to.eq(imageUrl)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an enabled notice banner with no text', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Notice banner')
|
||||
cy.switchField('Enable notice banner').click()
|
||||
cy.get('textarea[name="config.notice_banner.text"]').clear()
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Notice banner').should('have.attr', 'data-state', 'open')
|
||||
})
|
||||
|
||||
it('requires continuity values once an email inbox is linked', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Conversation continuity email inbox')
|
||||
linkEmailInbox(emailInboxName)
|
||||
cy.get('input[name="config.continuity.offline_threshold"]').clear()
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
|
||||
cy.get('input[name="config.continuity.offline_threshold"]').type('20m')
|
||||
cy.get('input[name="config.continuity.min_email_interval"]').clear()
|
||||
cy.get('input[name="config.continuity.min_email_interval"]').type('30m')
|
||||
cy.get('input[name="config.continuity.max_messages_per_email"]').clear()
|
||||
cy.get('input[name="config.continuity.max_messages_per_email"]').type('5')
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.continuity.offline_threshold).to.eq('20m')
|
||||
expect(config.continuity.min_email_interval).to.eq('30m')
|
||||
expect(config.continuity.max_messages_per_email).to.eq(5)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a continuity message count outside its range', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Conversation continuity email inbox')
|
||||
cy.get('input[name="config.continuity.max_messages_per_email"]').clear()
|
||||
cy.get('input[name="config.continuity.max_messages_per_email"]').type('200')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Conversation continuity email inbox').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'open'
|
||||
)
|
||||
})
|
||||
|
||||
it('saves the launch-directly toggle separately for each audience', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
enablePreChatForm()
|
||||
|
||||
cy.openInboxSection('Users')
|
||||
cy.switchFieldIn('Users', 'Launch directly into conversation').click()
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.prechat_form.enabled, 'pre-chat on').to.eq(true)
|
||||
expect(config.visitors.direct_to_conversation, 'visitors launch direct').to.eq(true)
|
||||
expect(config.users.direct_to_conversation, 'users launch direct').to.eq(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('adds a pre-chat custom attribute field and removes it again', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
enablePreChatForm()
|
||||
cy.inboxSection('Pre-chat form').contains('div.font-medium', attributeName).click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
const keys = config.prechat_form.visitors.fields.map((field) => field.key)
|
||||
expect(keys).to.include(attributeKey)
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.contains('div.font-medium', attributeName)
|
||||
.closest('div.border')
|
||||
.find('button[aria-label="Remove"]')
|
||||
.click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
const keys = config.prechat_form.visitors.fields.map((field) => field.key)
|
||||
expect(keys).to.not.include(attributeKey)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a business-hours-limited proactive message with no schedule picked', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.contains('button', 'Add proactive message').click()
|
||||
cy.get('#campaign-name').type(campaignName)
|
||||
cy.get('#campaign-message').type('Need a hand?')
|
||||
selectOption('#campaign-business_hours', 'During business hours')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.get('#campaign-hours').should('be.visible')
|
||||
|
||||
selectOption('#campaign-business_hours', 'Any time')
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.campaigns).to.have.length(1)
|
||||
expect(config.campaigns[0].business_hours).to.eq('any')
|
||||
})
|
||||
})
|
||||
|
||||
it('saves a proactive message condition and removes it again', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.contains('button', campaignName).click()
|
||||
cy.contains('button', 'Add condition').click()
|
||||
cy.contains('button[role="combobox"]', 'Select field').click()
|
||||
cy.get('[role="option"]').contains(attributeName).click()
|
||||
cy.contains('button[role="combobox"]', 'Select operator').click()
|
||||
cy.get('[role="option"]').contains('equals').click()
|
||||
cy.get('input[placeholder="Set value"]').type('gold')
|
||||
cy.inboxSection('Proactive messages').find('[role="radio"][value="OR"]').click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
const conditions = config.campaigns[0].conditions
|
||||
expect(conditions.logical_op).to.eq('OR')
|
||||
expect(conditions.rules).to.have.length(1)
|
||||
expect(conditions.rules[0].field).to.eq(attributeKey)
|
||||
expect(conditions.rules[0].value).to.eq('gold')
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.contains('button', campaignName).click()
|
||||
cy.inboxSection('Proactive messages').contains('button[role="combobox"]', attributeName)
|
||||
cy.inboxSection('Proactive messages').find('button[aria-label="Close"]').first().click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.campaigns[0].conditions.rules).to.have.length(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('saves a different pre-chat form for visitors and for users', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
enablePreChatForm()
|
||||
|
||||
cy.audienceTab('Pre-chat form', 'Visitors').click()
|
||||
audienceEnabledSwitch().should('have.attr', 'data-state', 'checked')
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.clear()
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.type('Visitor questions')
|
||||
|
||||
cy.audienceTab('Pre-chat form', 'Users').click()
|
||||
audienceEnabledSwitch().then(($switch) => {
|
||||
if ($switch.attr('data-state') !== 'checked') cy.wrap($switch).click()
|
||||
})
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.clear()
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.type('Signed-in questions')
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.prechat_form.visitors.enabled, 'visitors on').to.eq(true)
|
||||
expect(config.prechat_form.users.enabled, 'users on').to.eq(true)
|
||||
expect(config.prechat_form.visitors.title).to.eq('Visitor questions')
|
||||
expect(config.prechat_form.users.title).to.eq('Signed-in questions')
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.should('have.value', 'Visitor questions')
|
||||
cy.audienceTab('Pre-chat form', 'Users').click()
|
||||
cy.inboxSection('Pre-chat form')
|
||||
.find('input[placeholder="Tell us about yourself"]')
|
||||
.should('have.value', 'Signed-in questions')
|
||||
})
|
||||
|
||||
it('turns the pre-chat form off for visitors while it stays on for users', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
cy.audienceTab('Pre-chat form', 'Visitors').click()
|
||||
audienceEnabledSwitch().click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.prechat_form.enabled, 'form still on').to.eq(true)
|
||||
expect(config.prechat_form.visitors.enabled, 'visitors off').to.eq(false)
|
||||
expect(config.prechat_form.users.enabled, 'users on').to.eq(true)
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
cy.audienceTab('Pre-chat form', 'Visitors').click()
|
||||
audienceEnabledSwitch().should('have.attr', 'data-state', 'unchecked')
|
||||
cy.audienceTab('Pre-chat form', 'Users').click()
|
||||
audienceEnabledSwitch().should('have.attr', 'data-state', 'checked')
|
||||
})
|
||||
|
||||
it('keeps the pre-chat fields of one audience out of the other', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Pre-chat form')
|
||||
cy.audienceTab('Pre-chat form', 'Users').click()
|
||||
cy.inboxSection('Pre-chat form').contains('div.font-medium', attributeName).click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
const userKeys = config.prechat_form.users.fields.map((field) => field.key)
|
||||
const visitorKeys = config.prechat_form.visitors.fields.map((field) => field.key)
|
||||
expect(userKeys, 'users carry the attribute').to.include(attributeKey)
|
||||
expect(visitorKeys, 'visitors do not').to.not.include(attributeKey)
|
||||
})
|
||||
})
|
||||
|
||||
it('saves branding for each theme and keeps the halves apart', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Theme')
|
||||
cy.get('#theme-system').click()
|
||||
|
||||
cy.openInboxSection('Branding')
|
||||
cy.editBrandingTheme('Light')
|
||||
cy.get('#bg-solid').click()
|
||||
cy.get('input[name="config.branding.light.colors.primary"]').invoke(
|
||||
'val',
|
||||
'#0f766e'
|
||||
).trigger('input')
|
||||
cy.get('#text-black').click()
|
||||
|
||||
cy.editBrandingTheme('Dark')
|
||||
cy.get('#bg-solid').click()
|
||||
cy.get('input[name="config.branding.dark.colors.primary"]').invoke(
|
||||
'val',
|
||||
'#f59e0b'
|
||||
).trigger('input')
|
||||
cy.get('#text-white').click()
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.theme).to.eq('system')
|
||||
expect(config.branding.light.colors.primary).to.eq('#0f766e')
|
||||
expect(config.branding.dark.colors.primary).to.eq('#f59e0b')
|
||||
expect(config.branding.light.home_screen.header_text_color).to.eq('black')
|
||||
expect(config.branding.dark.home_screen.header_text_color).to.eq('white')
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Branding')
|
||||
cy.editBrandingTheme('Light')
|
||||
cy.get('input[name="config.branding.light.colors.primary"]').should('have.value', '#0f766e')
|
||||
cy.editBrandingTheme('Dark')
|
||||
cy.get('input[name="config.branding.dark.colors.primary"]').should('have.value', '#f59e0b')
|
||||
})
|
||||
|
||||
it('edits the dark half when the theme is forced to dark', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Theme')
|
||||
cy.get('#theme-dark').click()
|
||||
|
||||
cy.openInboxSection('Branding')
|
||||
cy.inboxSection('Branding')
|
||||
.find('[role="tab"][data-state="active"]')
|
||||
.should('contain.text', 'Dark')
|
||||
cy.get('input[name="config.branding.dark.logo_url"]').clear()
|
||||
cy.get('input[name="config.branding.dark.logo_url"]').type('https://cypress.test/dark.png')
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.theme).to.eq('dark')
|
||||
expect(config.branding.dark.logo_url).to.eq('https://cypress.test/dark.png')
|
||||
expect(config.branding.light.colors.primary, 'light half untouched').to.eq('#0f766e')
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Theme')
|
||||
cy.get('#theme-light').click()
|
||||
cy.openInboxSection('Branding')
|
||||
cy.inboxSection('Branding')
|
||||
.find('[role="tab"][data-state="active"]')
|
||||
.should('contain.text', 'Light')
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.theme).to.eq('light')
|
||||
expect(config.branding.dark.logo_url, 'dark half kept').to.eq('https://cypress.test/dark.png')
|
||||
})
|
||||
})
|
||||
|
||||
it('saves both gradient colours of the home screen background', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Branding')
|
||||
cy.editBrandingTheme('Light')
|
||||
cy.get('#bg-gradient').click()
|
||||
cy.get('input[name="config.branding.light.home_screen.background.gradient_start"]')
|
||||
.invoke('val', '#0f766e')
|
||||
.trigger('input')
|
||||
cy.get('input[name="config.branding.light.home_screen.background.gradient_end"]')
|
||||
.invoke('val', '#99f6e4')
|
||||
.trigger('input')
|
||||
cy.switchFieldIn('Branding', 'Fade background').click()
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
const background = config.branding.light.home_screen.background
|
||||
expect(background.type).to.eq('gradient')
|
||||
expect(background.gradient_start).to.eq('#0f766e')
|
||||
expect(background.gradient_end).to.eq('#99f6e4')
|
||||
expect(config.branding.light.home_screen.fade_background).to.eq(true)
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Branding')
|
||||
cy.editBrandingTheme('Light')
|
||||
cy.get('input[name="config.branding.light.home_screen.background.gradient_start"]').should(
|
||||
'have.value',
|
||||
'#0f766e'
|
||||
)
|
||||
cy.get('input[name="config.branding.light.home_screen.background.gradient_end"]').should(
|
||||
'have.value',
|
||||
'#99f6e4'
|
||||
)
|
||||
})
|
||||
|
||||
it('saves the proactive message sender, team, audience and cooldown', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.get('#campaign-cooldown').clear()
|
||||
cy.get('#campaign-cooldown').type('8h')
|
||||
cy.contains('button', campaignName).click()
|
||||
|
||||
// SelectAgentCombobox and SelectTeamCombobox drop the id they are given.
|
||||
pickFromLabelledSelect('Sender', 1)
|
||||
pickFromLabelledSelect('Route replies to team', 1)
|
||||
selectOption('#campaign-audience', 'Signed-in users')
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.campaign_cooldown).to.eq('8h')
|
||||
expect(config.campaigns[0].sender_id, 'sender picked').to.be.greaterThan(0)
|
||||
expect(config.campaigns[0].team_id, 'team picked').to.be.greaterThan(0)
|
||||
expect(config.campaigns[0].audience).to.eq('users')
|
||||
})
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.get('#campaign-cooldown').should('have.value', '8h')
|
||||
cy.contains('button', campaignName).click()
|
||||
cy.get('#campaign-audience').should('contain.text', 'Signed-in users')
|
||||
})
|
||||
|
||||
it('rejects a campaign cooldown that is not a duration', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.get('#campaign-cooldown').clear()
|
||||
cy.get('#campaign-cooldown').type('soon')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Proactive messages').should('have.attr', 'data-state', 'open')
|
||||
})
|
||||
|
||||
it('duplicates a proactive message', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Proactive messages')
|
||||
cy.contains('button', campaignName).click()
|
||||
cy.contains('button', 'Duplicate').click()
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.campaigns).to.have.length(2)
|
||||
const ids = config.campaigns.map((campaign) => campaign.id)
|
||||
expect(new Set(ids).size, 'each copy keeps its own id').to.eq(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a home screen app with a malformed link', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Home screen apps')
|
||||
cy.contains('button', 'Add external link').click()
|
||||
cy.get('[data-section="homeApps"] input[placeholder="Link Text"]').type('Status')
|
||||
cy.get('[data-section="homeApps"] input[placeholder="https://example.com"]').type('not-a-url')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Home screen apps').should('have.attr', 'data-state', 'open')
|
||||
})
|
||||
|
||||
it('rejects a quick reply longer than the limit', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Users')
|
||||
cy.get('textarea[name="config.visitors.quick_replies"]').clear()
|
||||
cy.get('textarea[name="config.visitors.quick_replies"]').type('x'.repeat(121), { delay: 0 })
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.get('@updateInbox.all').should('have.length', 0)
|
||||
cy.inboxSection('Users').should('have.attr', 'data-state', 'open')
|
||||
})
|
||||
|
||||
it('saves the auto language option', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('General')
|
||||
cy.inboxSection('General').contains('label', 'Language').parent().find('button').click()
|
||||
cy.get('[role="option"]').contains('Auto').click()
|
||||
cy.get('[role="option"]').should('not.exist')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
savedConfig('@updateInbox').then((config) => {
|
||||
expect(config.language).to.eq('auto')
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the help articles card out until a help center is linked', () => {
|
||||
editInbox(inboxId)
|
||||
cy.openInboxSection('Home screen apps')
|
||||
cy.contains('button', 'Add help articles').should('be.disabled')
|
||||
cy.contains('Choose a help center under Help center before adding this card.').should(
|
||||
'be.visible'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -173,16 +173,18 @@ describe('Live chat inbox form layout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the preview with its own theme toggle', () => {
|
||||
it('keeps the preview and the branding tabs on the same theme', () => {
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.get('[data-preview-theme="light"]').should('be.visible')
|
||||
|
||||
cy.openInboxSection('Branding')
|
||||
cy.editBrandingTheme('Dark')
|
||||
|
||||
cy.get('[data-preview-theme="light"]').should('have.attr', 'data-state', 'active')
|
||||
cy.get('[data-preview-theme="dark"]').click()
|
||||
cy.get('[data-preview-theme="dark"]').should('have.attr', 'data-state', 'active')
|
||||
|
||||
cy.get('[data-preview-theme="light"]').click()
|
||||
cy.inboxSection('Branding')
|
||||
.find('[role="tab"][data-state="active"]')
|
||||
.should('contain.text', 'Light')
|
||||
})
|
||||
|
||||
it('rejects more than six quick replies', () => {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
const stamp = Date.now()
|
||||
const inboxName = `Cypress Livechat ${stamp}`
|
||||
const renamedInbox = `Cypress Livechat ${stamp} edited`
|
||||
const brandName = `Cypress Brand ${stamp}`
|
||||
const websiteUrl = `https://cypress.test/${stamp}`
|
||||
const greeting = `Hello from Cypress ${stamp}`
|
||||
@@ -12,7 +11,6 @@ const editedBrandName = `Cypress Brand ${stamp} edited`
|
||||
const homeAppTitle = `Cypress Announcement ${stamp}`
|
||||
const editedHomeAppTitle = `Cypress Announcement ${stamp} edited`
|
||||
const secret = `${stamp}`.padEnd(32, 'c').slice(0, 32)
|
||||
const maskedSecret = '••••••••••'
|
||||
const newPath = '/admin/inboxes/new'
|
||||
const listPath = '/admin/inboxes'
|
||||
|
||||
@@ -71,52 +69,6 @@ describe('Live chat inbox form', () => {
|
||||
cy.contains(inboxName).should('exist')
|
||||
})
|
||||
|
||||
it('loads the saved values back into every tab of the edit form', () => {
|
||||
expect(inboxId, 'inbox from the create step').to.be.a('number')
|
||||
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('General')
|
||||
|
||||
cy.get('input[name="name"]').should('have.value', inboxName)
|
||||
cy.get('input[name="config.brand_name"]').should('have.value', brandName)
|
||||
cy.get('input[name="config.website_url"]').should('have.value', websiteUrl)
|
||||
|
||||
cy.openInboxSection('Launcher position')
|
||||
cy.get('input[name="config.launcher.spacing.side"]').should('have.value', '35')
|
||||
cy.get('input[name="config.launcher.spacing.bottom"]').should('have.value', '45')
|
||||
cy.get('select[name="config.launcher.position"]').should('have.value', 'left')
|
||||
|
||||
cy.openInboxSection('Messages')
|
||||
cy.get('textarea[name="config.greeting_message"]').should('have.value', greeting)
|
||||
cy.get('textarea[name="config.chat_introduction"]').should('have.value', chatIntroduction)
|
||||
|
||||
cy.openInboxSection('Security')
|
||||
cy.get('input[name="config.session_duration"]').should('have.value', '4h')
|
||||
cy.get('textarea[name="config.trusted_domains"]').should('have.value', trustedDomain)
|
||||
// The signing secret comes back masked, never as the real value.
|
||||
cy.get('input[name="secret"]').should('have.value', maskedSecret)
|
||||
})
|
||||
|
||||
it('persists a changed name and greeting message', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('General')
|
||||
cy.get('input[name="name"]').should('have.value', inboxName).clear().type(renamedInbox)
|
||||
|
||||
cy.openInboxSection('Messages')
|
||||
cy.get('textarea[name="config.greeting_message"]').clear().type(`${greeting} v2`)
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.wait('@updateInbox').its('response.statusCode').should('eq', 200)
|
||||
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('General')
|
||||
cy.get('input[name="name"]').should('have.value', renamedInbox)
|
||||
cy.openInboxSection('Messages')
|
||||
cy.get('textarea[name="config.greeting_message"]').should('have.value', `${greeting} v2`)
|
||||
})
|
||||
|
||||
it('rejects a submit with no name and no brand name', () => {
|
||||
cy.intercept('POST', '**/api/v1/inboxes').as('createInbox')
|
||||
|
||||
@@ -128,24 +80,6 @@ describe('Live chat inbox form', () => {
|
||||
cy.location('pathname').should('eq', newPath)
|
||||
})
|
||||
|
||||
it('rejects a malformed website url and jumps back to its tab', () => {
|
||||
cy.intercept('POST', '**/api/v1/inboxes').as('createInbox')
|
||||
|
||||
openNewForm()
|
||||
cy.get('input[name="name"]').type(inboxName)
|
||||
cy.get('input[name="config.brand_name"]').type(brandName)
|
||||
cy.get('input[name="config.website_url"]').type('not-a-url')
|
||||
|
||||
cy.openInboxSection('Security')
|
||||
cy.get('button[type="submit"]').click()
|
||||
|
||||
cy.contains('Invalid URL').scrollIntoView().should('be.visible')
|
||||
// The offending field is on another tab, so the form has to switch back to it.
|
||||
cy.get('input[name="config.website_url"]').scrollIntoView().should('be.visible')
|
||||
cy.get('[role="tab"][data-state="active"]').should('not.contain', 'Setup')
|
||||
cy.get('@createInbox.all').should('have.length', 0)
|
||||
})
|
||||
|
||||
it('persists a brand name edit made alongside a home screen app edit', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
@@ -178,12 +112,12 @@ describe('Live chat inbox form', () => {
|
||||
cy.intercept('DELETE', `**/api/v1/inboxes/${inboxId}`).as('deleteInbox')
|
||||
|
||||
cy.visit(listPath)
|
||||
filterList(renamedInbox)
|
||||
cy.contains('tr', renamedInbox).find('button[aria-haspopup="menu"]').click()
|
||||
filterList(inboxName)
|
||||
cy.contains('tr', inboxName).find('button[aria-haspopup="menu"]').click()
|
||||
cy.get('[role="menuitem"]').contains('Delete').click()
|
||||
cy.get('[role="alertdialog"]').contains('button', 'Delete').click()
|
||||
|
||||
cy.wait('@deleteInbox').its('response.statusCode').should('eq', 200)
|
||||
cy.contains(renamedInbox).should('not.exist')
|
||||
cy.contains(inboxName).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
const stamp = Date.now()
|
||||
const inboxName = `Cypress Theme ${stamp}`
|
||||
const brandName = `Cypress Theme Brand ${stamp}`
|
||||
const listPath = '/admin/inboxes'
|
||||
const newPath = '/admin/inboxes/new'
|
||||
|
||||
const lightPrimary = '#123456'
|
||||
@@ -24,6 +23,10 @@ describe('Live chat theme and branding', () => {
|
||||
cy.login()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
if (inboxId) cy.api('DELETE', `/api/v1/inboxes/${inboxId}`, null, { failOnStatusCode: false })
|
||||
})
|
||||
|
||||
it('saves a separate primary color for each theme', () => {
|
||||
cy.intercept('POST', '**/api/v1/inboxes').as('createInbox')
|
||||
|
||||
@@ -56,19 +59,6 @@ describe('Live chat theme and branding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads each theme back into its own tab', () => {
|
||||
expect(inboxId, 'inbox from the create step').to.be.a('number')
|
||||
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('Branding')
|
||||
|
||||
cy.editBrandingTheme('Light')
|
||||
cy.get('input[name="config.branding.light.colors.primary"]').should('have.value', lightPrimary)
|
||||
|
||||
cy.editBrandingTheme('Dark')
|
||||
cy.get('input[name="config.branding.dark.colors.primary"]').should('have.value', darkPrimary)
|
||||
})
|
||||
|
||||
it('seeds a dark background when the theme is switched away from light', () => {
|
||||
cy.intercept('POST', '**/api/v1/inboxes').as('createSeeded')
|
||||
|
||||
@@ -97,33 +87,4 @@ describe('Live chat theme and branding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a launcher icon scale outside its range', () => {
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('Launcher position')
|
||||
|
||||
cy.get('input[name="config.launcher.icon_scale"]').clear().type('10')
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.contains('40').should('exist')
|
||||
})
|
||||
|
||||
it('persists the launcher icon scale', () => {
|
||||
cy.intercept('PUT', `**/api/v1/inboxes/${inboxId}`).as('updateInbox')
|
||||
|
||||
cy.visit(`${listPath}/${inboxId}/edit`)
|
||||
cy.openInboxSection('Launcher position')
|
||||
cy.get('input[name="config.launcher.icon_scale"]').clear().type('60')
|
||||
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.wait('@updateInbox').then(({ response }) => {
|
||||
expect(response.statusCode).to.eq(200)
|
||||
expect(response.body.data.config.launcher.icon_scale, 'icon scale').to.eq(60)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
if (inboxId) {
|
||||
cy.login()
|
||||
cy.api('DELETE', `/api/v1/inboxes/${inboxId}`, null, { failOnStatusCode: false })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -129,9 +129,7 @@ describe('Live chat inbox form: proactive messages and help center tabs', () =>
|
||||
cy.get('#campaign-exclude_urls').clear().type('/checkout/*')
|
||||
cy.get('#campaign-delay_seconds').type('{selectall}25')
|
||||
cy.get('#campaign-event').clear().type('pricing_viewed')
|
||||
cy.contains(
|
||||
'Show this message only after your website tracks this event. Leave this field blank to show it without an event.'
|
||||
).should('be.visible')
|
||||
cy.contains('Show this message only after your website tracks this event.').should('be.visible')
|
||||
cy.contains('code', 'window.Libredesk.trackEvent("pricing_viewed")').should('be.visible')
|
||||
selectOption('#campaign-repeat', 'After an interval')
|
||||
cy.get('#campaign-repeat_hours').type('{selectall}12')
|
||||
|
||||
+8
-8
@@ -406,7 +406,7 @@
|
||||
"admin.inbox.livechat.preview.sampleAgentReply": "Happy to help, could you share a few details?",
|
||||
"admin.inbox.livechat.preview.sampleUserMessage": "Hi! How do I get started?",
|
||||
"admin.inbox.livechat.quickReplies": "Quick replies",
|
||||
"admin.inbox.livechat.quickReplies.description": "One reply per line, up to 6. Shown when someone starts a new conversation. Tapping one sends it as the first message.",
|
||||
"admin.inbox.livechat.quickReplies.description": "One reply per line, up to 6. Shown when someone starts a new conversation.",
|
||||
"admin.inbox.livechat.quickReplies.limit": "Add up to 6 quick replies.",
|
||||
"admin.inbox.livechat.quickReplies.placeholder": "I have a question about billing\nHow do I reset my password?",
|
||||
"admin.inbox.livechat.secretKey": "Secret key",
|
||||
@@ -1275,7 +1275,7 @@
|
||||
"helpCenter.styling.brand": "Brand",
|
||||
"helpCenter.styling.darkLogoUrl": "Logo URL (dark mode)",
|
||||
"helpCenter.styling.showThemeToggle": "Show light and dark mode toggle",
|
||||
"helpCenter.styling.themeToggleHint": "Visitors start in light mode and can switch to dark. Turn this off to keep the help center in light mode.",
|
||||
"helpCenter.styling.themeToggleHint": "Visitors start in light mode and can switch to dark.",
|
||||
"helpCenter.styling.customCode": "Custom code",
|
||||
"helpCenter.customJS": "Custom JS",
|
||||
"helpCenter.deleteArticleConfirmation": "This will permanently delete the article.",
|
||||
@@ -1328,7 +1328,7 @@
|
||||
"helpCenter.translationsHint": "Write or link this article in other languages here. Readers who switch language land on the linked version, instead of the help center home page.",
|
||||
"helpCenter.linkExistingArticle": "Link an existing article",
|
||||
"helpCenter.linkExistingArticleHint": "Pick an article written in another language to join it with this one as a translation.",
|
||||
"helpCenter.noLinkableArticles": "No articles in the other languages are free to link. Articles already linked as a translation are left out. Write the translation instead.",
|
||||
"helpCenter.noLinkableArticles": "No articles in the other languages are free to link. Write the translation instead.",
|
||||
"helpCenter.articleAlreadyTranslated": "That article is already part of another set of translations. Unlink it there first.",
|
||||
"helpCenter.localeAlreadyTranslated": "This article already has a translation in that language.",
|
||||
"helpCenter.cannotLinkArticleToItself": "An article cannot be its own translation.",
|
||||
@@ -1704,9 +1704,9 @@
|
||||
"widget.campaignContactAttributesEmpty": "Visibility conditions use contact custom attributes. None are available.",
|
||||
"widget.campaignCooldown": "Minimum time between proactive messages",
|
||||
"widget.campaignDeviceRequired": "Select at least one device. This proactive message cannot appear until desktop or mobile is enabled.",
|
||||
"widget.campaignEventHint": "Show this message only after your website tracks this event. Leave this field blank to show it without an event.",
|
||||
"widget.campaignEventHint": "Show this message only after your website tracks this event.",
|
||||
"widget.campaignLimitReached": "You have reached the limit of 50 proactive messages. Delete one to add another.",
|
||||
"widget.campaignPriorityHint": "Messages are checked from top to bottom. The first matching message is shown.",
|
||||
"widget.campaignPriorityHint": "Messages are checked from top to bottom. The first match is shown.",
|
||||
"widget.campaignsHint": "Show a message on matching pages before the chat is opened. A conversation is created only when someone replies to it.",
|
||||
"widget.campaignStats": "Proactive message results",
|
||||
"widget.campaignUrlLimit": "Add up to 20 page patterns.",
|
||||
@@ -1717,10 +1717,10 @@
|
||||
"widget.featuredArticles": "Featured articles",
|
||||
"widget.featuredArticlesHint": "Choose up to 10 articles. Leave empty to show popular articles.",
|
||||
"widget.helpFeedbackError": "Feedback could not be saved. Try again.",
|
||||
"widget.helpHomeAppHint": "Shows a search button and the featured articles set in the Help center tab.",
|
||||
"widget.helpHomeAppRequiresCenter": "Choose a help center in the Help center tab before adding this card.",
|
||||
"widget.helpHomeAppHint": "Shows a search button and the featured articles set under Help center.",
|
||||
"widget.helpHomeAppRequiresCenter": "Choose a help center under Help center before adding this card.",
|
||||
"widget.helpLoadError": "Help could not be loaded. Try again or send us a message.",
|
||||
"widget.helpOnHomeHint": "To show help on the Home screen, add the help articles card under Appearance, Home screen apps.",
|
||||
"widget.helpOnHomeHint": "To show help on the Home screen, add the help articles card under Home screen apps.",
|
||||
"widget.helpPlacement": "Where help appears",
|
||||
"widget.invitationExpired": "This invitation is no longer available. Start a new chat to contact us.",
|
||||
"widget.ipBlocked": "Access denied",
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
amodels "github.com/abhinavxd/libredesk/internal/automation/models"
|
||||
"github.com/abhinavxd/libredesk/internal/conversation/models"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox"
|
||||
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat/proactive"
|
||||
@@ -33,6 +34,13 @@ const (
|
||||
ThemeDark = "dark"
|
||||
|
||||
DefaultLauncherIconScale = 100
|
||||
DefaultCampaignCooldown = "24h"
|
||||
DefaultLauncherColor = "#000000"
|
||||
BackgroundSolid = "solid"
|
||||
// Matches the locale the widget falls back to when this is unset.
|
||||
DefaultLanguage = "en-US"
|
||||
// Matches the session TTL the chat handlers fall back to when this is unset.
|
||||
DefaultSessionDuration = "4320h"
|
||||
)
|
||||
|
||||
type Colors struct {
|
||||
@@ -287,9 +295,82 @@ func (c *Config) UnmarshalJSON(data []byte) error {
|
||||
if c.Launcher.IconScale == 0 {
|
||||
c.Launcher.IconScale = DefaultLauncherIconScale
|
||||
}
|
||||
if c.CampaignCooldown == "" {
|
||||
c.CampaignCooldown = DefaultCampaignCooldown
|
||||
}
|
||||
if c.SessionDuration == "" {
|
||||
c.SessionDuration = DefaultSessionDuration
|
||||
}
|
||||
if c.Language == "" {
|
||||
c.Language = DefaultLanguage
|
||||
}
|
||||
if c.FallbackLanguage == "" {
|
||||
c.FallbackLanguage = DefaultLanguage
|
||||
}
|
||||
fillBrandingDefaults(&c.Branding.Light, ThemeLight)
|
||||
fillBrandingDefaults(&c.Branding.Dark, ThemeDark)
|
||||
c.fillEmptyLists()
|
||||
return nil
|
||||
}
|
||||
|
||||
func fillBrandingDefaults(branding *Branding, theme string) {
|
||||
if branding.Launcher.Color == "" {
|
||||
branding.Launcher.Color = DefaultLauncherColor
|
||||
}
|
||||
if branding.HomeScreen.Background.Type == "" {
|
||||
branding.HomeScreen.Background.Type = BackgroundSolid
|
||||
}
|
||||
if branding.HomeScreen.HeaderTextColor == "" {
|
||||
if theme == ThemeDark {
|
||||
branding.HomeScreen.HeaderTextColor = "white"
|
||||
} else {
|
||||
branding.HomeScreen.HeaderTextColor = "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A null list fails the admin form's validation.
|
||||
func (c *Config) fillEmptyLists() {
|
||||
if c.Campaigns == nil {
|
||||
c.Campaigns = []proactive.Campaign{}
|
||||
}
|
||||
for i := range c.Campaigns {
|
||||
if c.Campaigns[i].IncludeURLs == nil {
|
||||
c.Campaigns[i].IncludeURLs = []string{}
|
||||
}
|
||||
if c.Campaigns[i].ExcludeURLs == nil {
|
||||
c.Campaigns[i].ExcludeURLs = []string{}
|
||||
}
|
||||
if c.Campaigns[i].Conditions.Rules == nil {
|
||||
c.Campaigns[i].Conditions.Rules = []amodels.RuleDetail{}
|
||||
}
|
||||
}
|
||||
if c.Help.FeaturedIDs == nil {
|
||||
c.Help.FeaturedIDs = []int{}
|
||||
}
|
||||
if c.HomeApps == nil {
|
||||
c.HomeApps = []HomeApp{}
|
||||
}
|
||||
if c.TrustedDomains == nil {
|
||||
c.TrustedDomains = []string{}
|
||||
}
|
||||
if c.BlockedIPs == nil {
|
||||
c.BlockedIPs = []string{}
|
||||
}
|
||||
if c.QuickReplies == nil {
|
||||
c.QuickReplies = []string{}
|
||||
}
|
||||
if c.Users.QuickReplies == nil {
|
||||
c.Users.QuickReplies = []string{}
|
||||
}
|
||||
if c.Visitors.QuickReplies == nil {
|
||||
c.Visitors.QuickReplies = []string{}
|
||||
}
|
||||
if c.PreChatForm.Fields == nil {
|
||||
c.PreChatForm.Fields = []PreChatFormField{}
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) ResolvePreChatForm(isVisitor bool) Config {
|
||||
audience := c.PreChatForm.Users
|
||||
if isVisitor {
|
||||
|
||||
@@ -138,3 +138,64 @@ func TestUnmarshalKeepsExplicitBranding(t *testing.T) {
|
||||
t.Fatalf("branding = %+v", config.Branding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalFillsEmptyListsAndCooldown(t *testing.T) {
|
||||
var config Config
|
||||
if err := json.Unmarshal([]byte(`{"brand_name":"Acme"}`), &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, key := range []string{"campaigns", "home_apps", "trusted_domains", "blocked_ips", "quick_replies"} {
|
||||
if decoded[key] == nil {
|
||||
t.Fatalf("%s encoded as null", key)
|
||||
}
|
||||
}
|
||||
if config.Help.FeaturedIDs == nil || config.PreChatForm.Fields == nil {
|
||||
t.Fatalf("help featured ids = %v, prechat fields = %v", config.Help.FeaturedIDs, config.PreChatForm.Fields)
|
||||
}
|
||||
if config.Users.QuickReplies == nil || config.Visitors.QuickReplies == nil {
|
||||
t.Fatalf("audience quick replies = %v, %v", config.Users.QuickReplies, config.Visitors.QuickReplies)
|
||||
}
|
||||
if config.CampaignCooldown != DefaultCampaignCooldown {
|
||||
t.Fatalf("campaign cooldown = %q", config.CampaignCooldown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalFillsValuesAConfigWrittenOutsideTheFormLacks(t *testing.T) {
|
||||
var config Config
|
||||
if err := json.Unmarshal([]byte(`{"brand_name":"Acme","colors":{"primary":"#112233"},"launcher":{"position":"right"}}`), &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if config.SessionDuration != DefaultSessionDuration {
|
||||
t.Fatalf("session duration = %q", config.SessionDuration)
|
||||
}
|
||||
if config.Language != DefaultLanguage || config.FallbackLanguage != DefaultLanguage {
|
||||
t.Fatalf("language = %q, fallback = %q", config.Language, config.FallbackLanguage)
|
||||
}
|
||||
for theme, branding := range map[string]Branding{ThemeLight: config.Branding.Light, ThemeDark: config.Branding.Dark} {
|
||||
if branding.Launcher.Color != DefaultLauncherColor {
|
||||
t.Fatalf("%s launcher color = %q", theme, branding.Launcher.Color)
|
||||
}
|
||||
if branding.HomeScreen.Background.Type != BackgroundSolid {
|
||||
t.Fatalf("%s background type = %q", theme, branding.HomeScreen.Background.Type)
|
||||
}
|
||||
if branding.Colors.Primary != "#112233" {
|
||||
t.Fatalf("%s primary = %q", theme, branding.Colors.Primary)
|
||||
}
|
||||
}
|
||||
if config.Branding.Light.HomeScreen.HeaderTextColor != "black" {
|
||||
t.Fatalf("light header text = %q", config.Branding.Light.HomeScreen.HeaderTextColor)
|
||||
}
|
||||
if config.Branding.Dark.HomeScreen.HeaderTextColor != "white" {
|
||||
t.Fatalf("dark header text = %q", config.Branding.Dark.HomeScreen.HeaderTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user