2
0

♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@ -0,0 +1,87 @@
import React from 'react'
import { AvatarProps } from 'models'
import {
Heading,
HStack,
Popover,
PopoverContent,
PopoverTrigger,
Stack,
Switch,
Image,
Flex,
Box,
Portal,
} from '@chakra-ui/react'
import { ImageUploadContent } from '@/components/ImageUploadContent'
import { DefaultAvatar } from '../DefaultAvatar'
type Props = {
uploadFilePath: string
title: string
avatarProps?: AvatarProps
isDefaultCheck?: boolean
onAvatarChange: (avatarProps: AvatarProps) => void
}
export const AvatarForm = ({
uploadFilePath,
title,
avatarProps,
isDefaultCheck = false,
onAvatarChange,
}: Props) => {
const isChecked = avatarProps ? avatarProps.isEnabled : isDefaultCheck
const handleOnCheck = () =>
onAvatarChange({ ...avatarProps, isEnabled: !isChecked })
const handleImageUrl = (url: string) =>
onAvatarChange({ isEnabled: isChecked, url })
const isDefaultAvatar = !avatarProps?.url || avatarProps.url.includes('{{')
return (
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Flex justifyContent="space-between">
<HStack>
<Heading as="label" fontSize="lg" htmlFor={title} mb="1">
{title}
</Heading>
<Switch isChecked={isChecked} id={title} onChange={handleOnCheck} />
</HStack>
{isChecked && (
<Popover isLazy>
<PopoverTrigger>
{isDefaultAvatar ? (
<Box>
<DefaultAvatar
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
/>
</Box>
) : (
<Image
src={avatarProps.url}
alt="Website image"
cursor="pointer"
_hover={{ filter: 'brightness(.9)' }}
transition="filter 200ms"
rounded="full"
boxSize="40px"
objectFit="cover"
/>
)}
</PopoverTrigger>
<Portal>
<PopoverContent p="4">
<ImageUploadContent
filePath={uploadFilePath}
defaultUrl={avatarProps?.url}
onSubmit={handleImageUrl}
/>
</PopoverContent>
</Portal>
</Popover>
)}
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
buttons: ContainerColors
onButtonsChange: (buttons: ContainerColors) => void
}
export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onButtonsChange({ ...buttons, backgroundColor })
const handleTextChange = (color: string) =>
onButtonsChange({ ...buttons, color })
return (
<Stack data-testid="buttons-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={buttons.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={buttons.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,80 @@
import { Heading, Stack } from '@chakra-ui/react'
import { AvatarProps, ChatTheme, ContainerColors, InputColors } from 'models'
import React from 'react'
import { AvatarForm } from './AvatarForm'
import { ButtonsTheme } from './ButtonsTheme'
import { GuestBubbles } from './GuestBubbles'
import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme'
type Props = {
typebotId: string
chatTheme: ChatTheme
onChatThemeChange: (chatTheme: ChatTheme) => void
}
export const ChatThemeSettings = ({
typebotId,
chatTheme,
onChatThemeChange,
}: Props) => {
const handleHostBubblesChange = (hostBubbles: ContainerColors) =>
onChatThemeChange({ ...chatTheme, hostBubbles })
const handleGuestBubblesChange = (guestBubbles: ContainerColors) =>
onChatThemeChange({ ...chatTheme, guestBubbles })
const handleButtonsChange = (buttons: ContainerColors) =>
onChatThemeChange({ ...chatTheme, buttons })
const handleInputsChange = (inputs: InputColors) =>
onChatThemeChange({ ...chatTheme, inputs })
const handleHostAvatarChange = (hostAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, hostAvatar })
const handleGuestAvatarChange = (guestAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, guestAvatar })
return (
<Stack spacing={6}>
<AvatarForm
uploadFilePath={`typebots/${typebotId}/hostAvatar`}
title="Bot avatar"
avatarProps={chatTheme.hostAvatar}
isDefaultCheck
onAvatarChange={handleHostAvatarChange}
/>
<AvatarForm
uploadFilePath={`typebots/${typebotId}/guestAvatar`}
title="User avatar"
avatarProps={chatTheme.guestAvatar}
onAvatarChange={handleGuestAvatarChange}
/>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Bot bubbles</Heading>
<HostBubbles
hostBubbles={chatTheme.hostBubbles}
onHostBubblesChange={handleHostBubblesChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">User bubbles</Heading>
<GuestBubbles
guestBubbles={chatTheme.guestBubbles}
onGuestBubblesChange={handleGuestBubblesChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Buttons</Heading>
<ButtonsTheme
buttons={chatTheme.buttons}
onButtonsChange={handleButtonsChange}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Inputs</Heading>
<InputsTheme
inputs={chatTheme.inputs}
onInputsChange={handleInputsChange}
/>
</Stack>
</Stack>
)
}

View File

@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
guestBubbles: ContainerColors
onGuestBubblesChange: (hostBubbles: ContainerColors) => void
}
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onGuestBubblesChange({ ...guestBubbles, backgroundColor })
const handleTextChange = (color: string) =>
onGuestBubblesChange({ ...guestBubbles, color })
return (
<Stack data-testid="guest-bubbles-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={guestBubbles.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={guestBubbles.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,35 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
hostBubbles: ContainerColors
onHostBubblesChange: (hostBubbles: ContainerColors) => void
}
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onHostBubblesChange({ ...hostBubbles, backgroundColor })
const handleTextChange = (color: string) =>
onHostBubblesChange({ ...hostBubbles, color })
return (
<Stack data-testid="host-bubbles-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={hostBubbles.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={hostBubbles.color}
onColorChange={handleTextChange}
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1,44 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { InputColors } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
type Props = {
inputs: InputColors
onInputsChange: (buttons: InputColors) => void
}
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor })
const handleTextChange = (color: string) =>
onInputsChange({ ...inputs, color })
const handlePlaceholderChange = (placeholderColor: string) =>
onInputsChange({ ...inputs, placeholderColor })
return (
<Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center">
<Text>Background:</Text>
<ColorPicker
initialColor={inputs.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Text:</Text>
<ColorPicker
initialColor={inputs.color}
onColorChange={handleTextChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>Placeholder text:</Text>
<ColorPicker
initialColor={inputs.placeholderColor}
onColorChange={handlePlaceholderChange}
/>
</Flex>
</Stack>
)
}

View File

@ -0,0 +1 @@
export { ChatThemeSettings } from './ChatThemeSettings'

View File

@ -0,0 +1,17 @@
import { CodeEditor } from '@/components/CodeEditor'
import React from 'react'
type Props = {
customCss?: string
onCustomCssChange: (css: string) => void
}
export const CustomCssSettings = ({ customCss, onCustomCssChange }: Props) => {
return (
<CodeEditor
value={customCss ?? ''}
lang="css"
onChange={onCustomCssChange}
/>
)
}

View File

@ -0,0 +1,38 @@
import { Icon, IconProps } from '@chakra-ui/react'
import React from 'react'
export const DefaultAvatar = (props: IconProps) => {
return (
<Icon
viewBox="0 0 75 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
boxSize="40px"
data-testid="default-avatar"
{...props}
>
<mask id="mask0" x="0" y="0" mask-type="alpha">
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
</mask>
<g mask="url(#mask0)">
<rect x="-30" y="-43" width="131" height="154" fill="#0042DA" />
<rect
x="2.50413"
y="120.333"
width="81.5597"
height="86.4577"
rx="2.5"
transform="rotate(-52.6423 2.50413 120.333)"
stroke="#FED23D"
strokeWidth="5"
/>
<circle cx="76.5" cy="-1.5" r="29" stroke="#FF8E20" strokeWidth="5" />
<path
d="M-49.8224 22L-15.5 -40.7879L18.8224 22H-49.8224Z"
stroke="#F7F8FF"
strokeWidth="5"
/>
</g>
</Icon>
)
}

View File

@ -0,0 +1,40 @@
import { Flex, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'models'
import React from 'react'
import { ColorPicker } from '../../../../../components/ColorPicker'
type BackgroundContentProps = {
background?: Background
onBackgroundContentChange: (content: string) => void
}
const defaultBackgroundColor = '#ffffff'
export const BackgroundContent = ({
background,
onBackgroundContentChange,
}: BackgroundContentProps) => {
const handleContentChange = (content: string) =>
onBackgroundContentChange(content)
switch (background?.type) {
case BackgroundType.COLOR:
return (
<Flex justify="space-between" align="center">
<Text>Background color:</Text>
<ColorPicker
initialColor={background.content ?? defaultBackgroundColor}
onColorChange={handleContentChange}
/>
</Flex>
)
case BackgroundType.IMAGE:
return (
<Flex>
<Text>Image</Text>
</Flex>
)
default:
return <></>
}
}

View File

@ -0,0 +1,37 @@
import { Stack, Text } from '@chakra-ui/react'
import { Background, BackgroundType } from 'models'
import React from 'react'
import { BackgroundContent } from './BackgroundContent'
import { BackgroundTypeRadioButtons } from './BackgroundTypeRadioButtons'
type Props = {
background?: Background
onBackgroundChange: (newBackground: Background) => void
}
const defaultBackgroundType = BackgroundType.NONE
export const BackgroundSelector = ({
background,
onBackgroundChange,
}: Props) => {
const handleBackgroundTypeChange = (type: BackgroundType) =>
background && onBackgroundChange({ ...background, type })
const handleBackgroundContentChange = (content: string) =>
background && onBackgroundChange({ ...background, content })
return (
<Stack spacing={4}>
<Text>Background</Text>
<BackgroundTypeRadioButtons
backgroundType={background?.type ?? defaultBackgroundType}
onBackgroundTypeChange={handleBackgroundTypeChange}
/>
<BackgroundContent
background={background}
onBackgroundContentChange={handleBackgroundContentChange}
/>
</Stack>
)
}

View File

@ -0,0 +1,73 @@
import {
Box,
Flex,
HStack,
useRadio,
useRadioGroup,
UseRadioProps,
} from '@chakra-ui/react'
import { BackgroundType } from 'models'
import { ReactNode } from 'react'
type Props = {
backgroundType: BackgroundType
onBackgroundTypeChange: (type: BackgroundType) => void
}
export const BackgroundTypeRadioButtons = ({
backgroundType,
onBackgroundTypeChange,
}: Props) => {
const options = ['Color', 'None']
const { getRootProps, getRadioProps } = useRadioGroup({
name: 'background-type',
defaultValue: backgroundType,
onChange: (nextVal: string) =>
onBackgroundTypeChange(nextVal as BackgroundType),
})
const group = getRootProps()
return (
<HStack {...group}>
{options.map((value) => {
const radio = getRadioProps({ value })
return (
<RadioCard key={value} {...radio}>
{value}
</RadioCard>
)
})}
</HStack>
)
}
export const RadioCard = (props: UseRadioProps & { children: ReactNode }) => {
const { getInputProps, getCheckboxProps } = useRadio(props)
const input = getInputProps()
const checkbox = getCheckboxProps()
return (
<Box as="label" flex="1">
<input {...input} />
<Flex
{...checkbox}
cursor="pointer"
borderWidth="1px"
borderRadius="md"
_checked={{
bg: 'orange.400',
color: 'white',
borderColor: 'orange.400',
}}
px={5}
py={2}
transition="background-color 150ms, color 150ms, border 150ms"
justifyContent="center"
>
{props.children}
</Flex>
</Box>
)
}

View File

@ -0,0 +1 @@
export { BackgroundSelector } from './BackgroundSelector'

View File

@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react'
import { Text, HStack } from '@chakra-ui/react'
import { SearchableDropdown } from '@/components/SearchableDropdown'
import { env, isEmpty } from 'utils'
type FontSelectorProps = {
activeFont?: string
onSelectFont: (font: string) => void
}
export const FontSelector = ({
activeFont,
onSelectFont,
}: FontSelectorProps) => {
const [currentFont, setCurrentFont] = useState(activeFont)
const [googleFonts, setGoogleFonts] = useState<string[]>([])
useEffect(() => {
fetchPopularFonts().then(setGoogleFonts)
}, [])
const fetchPopularFonts = async () => {
if (isEmpty(env('GOOGLE_API_KEY'))) return []
const response = await fetch(
`https://www.googleapis.com/webfonts/v1/webfonts?key=${env(
'GOOGLE_API_KEY'
)}&sort=popularity`
)
return (await response.json()).items.map(
(item: { family: string }) => item.family
)
}
const handleFontSelected = (nextFont: string) => {
if (nextFont == currentFont) return
setCurrentFont(nextFont)
onSelectFont(nextFont)
}
return (
<HStack justify="space-between" align="center">
<Text>Font</Text>
<SearchableDropdown
selectedItem={activeFont}
items={googleFonts}
onValueChange={handleFontSelected}
/>
</HStack>
)
}

View File

@ -0,0 +1 @@
export { FontSelector } from './FontSelector'

View File

@ -0,0 +1,34 @@
import { Stack } from '@chakra-ui/react'
import { Background, GeneralTheme } from 'models'
import React from 'react'
import { BackgroundSelector } from './BackgroundSelector'
import { FontSelector } from './FontSelector'
type Props = {
generalTheme: GeneralTheme
onGeneralThemeChange: (general: GeneralTheme) => void
}
export const GeneralSettings = ({
generalTheme,
onGeneralThemeChange,
}: Props) => {
const handleSelectFont = (font: string) =>
onGeneralThemeChange({ ...generalTheme, font })
const handleBackgroundChange = (background: Background) =>
onGeneralThemeChange({ ...generalTheme, background })
return (
<Stack spacing={6}>
<FontSelector
activeFont={generalTheme.font}
onSelectFont={handleSelectFont}
/>
<BackgroundSelector
background={generalTheme.background}
onBackgroundChange={handleBackgroundChange}
/>
</Stack>
)
}

View File

@ -0,0 +1 @@
export { GeneralSettings } from './GeneralSettings'

View File

@ -0,0 +1,30 @@
import { Seo } from '@/components/Seo'
import { TypebotHeader, useTypebot } from '@/features/editor'
import { Flex } from '@chakra-ui/react'
import { TypebotViewer } from 'bot-engine'
import { getViewerUrl } from 'utils'
import { ThemeSideMenu } from './ThemeSideMenu'
import { parseTypebotToPublicTypebot } from '@/features/publish'
export const ThemePage = () => {
const { typebot } = useTypebot()
const publicTypebot = typebot && parseTypebotToPublicTypebot(typebot)
return (
<Flex overflow="hidden" h="100vh" flexDir="column">
<Seo title="Theme" />
<TypebotHeader />
<Flex h="full" w="full">
<ThemeSideMenu />
<Flex flex="1">
{publicTypebot && (
<TypebotViewer
apiHost={getViewerUrl({ isBuilder: true })}
typebot={publicTypebot}
/>
)}
</Flex>
</Flex>
</Flex>
)
}

View File

@ -0,0 +1,101 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Heading,
HStack,
Stack,
} from '@chakra-ui/react'
import { ChatIcon, CodeIcon, PencilIcon } from '@/components/icons'
import { ChatTheme, GeneralTheme } from 'models'
import React from 'react'
import { ChatThemeSettings } from './ChatSettings'
import { CustomCssSettings } from './CustomCssSettings/CustomCssSettings'
import { GeneralSettings } from './GeneralSettings'
import { headerHeight, useTypebot } from '@/features/editor'
export const ThemeSideMenu = () => {
const { typebot, updateTypebot } = useTypebot()
const handleChatThemeChange = (chat: ChatTheme) =>
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
const handleGeneralThemeChange = (general: GeneralTheme) =>
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
const handleCustomCssChange = (customCss: string) =>
typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
return (
<Stack
flex="1"
maxW="400px"
height={`calc(100vh - ${headerHeight}px)`}
borderRightWidth={1}
pt={10}
spacing={10}
overflowY="scroll"
pb="20"
>
<Heading fontSize="xl" textAlign="center">
Customize the theme
</Heading>
<Accordion allowMultiple defaultIndex={[0]}>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<PencilIcon />
<Heading fontSize="lg">General</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<GeneralSettings
generalTheme={typebot.theme.general}
onGeneralThemeChange={handleGeneralThemeChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<ChatIcon />
<Heading fontSize="lg">Chat</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<ChatThemeSettings
typebotId={typebot.id}
chatTheme={typebot.theme.chat}
onChatThemeChange={handleChatThemeChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<CodeIcon />
<Heading fontSize="lg">Custom CSS</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
{typebot && (
<CustomCssSettings
customCss={typebot.theme.customCss}
onCustomCssChange={handleCustomCssChange}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@ -0,0 +1 @@
export { ThemePage } from './components/ThemePage'

View File

@ -0,0 +1,184 @@
import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test'
import cuid from 'cuid'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import { typebotViewer } from 'utils/playwright/testHelpers'
const hostAvatarUrl =
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80'
const guestAvatarUrl =
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
test.describe.parallel('Theme page', () => {
test.describe('General', () => {
test('should reflect change in real-time', async ({ page }) => {
const typebotId = cuid()
const chatContainer = typebotViewer(page).locator(
'[data-testid="container"]'
)
await importTypebotInDatabase(getTestAsset('typebots/theme.json'), {
id: typebotId,
})
await page.goto(`/typebots/${typebotId}/theme`)
await expect(
typebotViewer(page).locator('button >> text="Go"')
).toBeVisible()
// Font
await page.fill('input[type="text"]', 'Roboto Slab')
await expect(chatContainer).toHaveCSS('font-family', '"Roboto Slab"')
// BG color
await expect(chatContainer).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)'
)
await page.click('text=Color')
await page.click('[aria-label="Pick a color"]')
await page.fill('[aria-label="Color value"] >> nth=-1', '#2a9d8f')
await expect(chatContainer).toHaveCSS(
'background-color',
'rgb(42, 157, 143)'
)
})
})
test.describe('Chat', () => {
test('should reflect change in real-time', async ({ page }) => {
const typebotId = 'chat-theme-typebot'
try {
await importTypebotInDatabase(getTestAsset('typebots/theme.json'), {
id: typebotId,
})
} catch {}
await page.goto(`/typebots/${typebotId}/theme`)
await expect(
typebotViewer(page).locator('button >> text="Go"')
).toBeVisible()
await page.click('button:has-text("Chat")')
// Host avatar
await expect(
typebotViewer(page).locator('[data-testid="default-avatar"]')
).toBeVisible()
await page.click('[data-testid="default-avatar"]')
await page.click('button:has-text("Embed link")')
await page.fill(
'input[placeholder="Paste the image link..."]',
hostAvatarUrl
)
await typebotViewer(page).locator('button >> text="Go"').click()
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
'src',
hostAvatarUrl
)
await page.click('text=Bot avatar')
await expect(typebotViewer(page).locator('img')).toBeHidden()
// Host bubbles
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#F7F8FF"]', '#2a9d8f')
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#ffffff')
const hostBubble = typebotViewer(page).locator(
'[data-testid="host-bubble"] >> nth=-1'
)
await expect(hostBubble).toHaveCSS(
'background-color',
'rgb(42, 157, 143)'
)
await expect(hostBubble).toHaveCSS('color', 'rgb(255, 255, 255)')
// Buttons
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#0042DA"]', '#7209b7')
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#e9c46a')
const button = typebotViewer(page).locator('[data-testid="button"]')
await expect(button).toHaveCSS('background-color', 'rgb(114, 9, 183)')
await expect(button).toHaveCSS('color', 'rgb(233, 196, 106)')
// Guest bubbles
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FF8E21"]', '#d8f3dc')
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#264653')
await typebotViewer(page).locator('button >> text="Go"').click()
const guestBubble = typebotViewer(page).locator(
'[data-testid="guest-bubble"] >> nth=-1'
)
await expect(guestBubble).toHaveCSS(
'background-color',
'rgb(216, 243, 220)'
)
await expect(guestBubble).toHaveCSS('color', 'rgb(38, 70, 83)')
// Guest avatar
await page.click('text=User avatar')
await expect(
typebotViewer(page).locator('[data-testid="default-avatar"] >> nth=-1')
).toBeVisible()
await page.click('[data-testid="default-avatar"]')
await page.click('button:has-text("Embed link")')
await page.fill(
'input[placeholder="Paste the image link..."]',
guestAvatarUrl
)
typebotViewer(page).locator('button >> text="Go"').click()
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
'src',
guestAvatarUrl
)
// Input
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FFFFFF"]', '#ffe8d6')
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#023e8a')
const input = typebotViewer(page).locator('.typebot-input')
await expect(input).toHaveCSS('background-color', 'rgb(255, 232, 214)')
await expect(input).toHaveCSS('color', 'rgb(2, 62, 138)')
})
})
test.describe('Custom CSS', () => {
test('should reflect change in real-time', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(getTestAsset('typebots/theme.json'), {
id: typebotId,
})
await page.goto(`/typebots/${typebotId}/theme`)
await expect(
typebotViewer(page).locator('button >> text="Go"')
).toBeVisible()
await page.click('button:has-text("Custom CSS")')
await page.fill(
'div[role="textbox"]',
'.typebot-button {background-color: green}'
)
await expect(
typebotViewer(page).locator('[data-testid="button"]')
).toHaveCSS('background-color', 'rgb(0, 128, 0)')
})
})
})