♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@ -0,0 +1,95 @@
|
||||
import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { Plan } from 'db'
|
||||
import { GeneralSettings } from 'models'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { ChangePlanModal, isFreePlan, LimitReached } from '@/features/billing'
|
||||
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
|
||||
import { LockTag } from '@/features/billing'
|
||||
|
||||
type Props = {
|
||||
generalSettings: GeneralSettings
|
||||
onGeneralSettingsChange: (generalSettings: GeneralSettings) => void
|
||||
}
|
||||
|
||||
export const GeneralSettingsForm = ({
|
||||
generalSettings,
|
||||
onGeneralSettingsChange,
|
||||
}: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const { workspace } = useWorkspace()
|
||||
const isWorkspaceFreePlan = isFreePlan(workspace)
|
||||
const handleSwitchChange = () => {
|
||||
if (generalSettings?.isBrandingEnabled && isWorkspaceFreePlan) return
|
||||
onGeneralSettingsChange({
|
||||
...generalSettings,
|
||||
isBrandingEnabled: !generalSettings?.isBrandingEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
const handleNewResultOnRefreshChange = (isRememberSessionChecked: boolean) =>
|
||||
onGeneralSettingsChange({
|
||||
...generalSettings,
|
||||
isNewResultOnRefreshEnabled: !isRememberSessionChecked,
|
||||
})
|
||||
|
||||
const handleInputPrefillChange = (isInputPrefillEnabled: boolean) =>
|
||||
onGeneralSettingsChange({
|
||||
...generalSettings,
|
||||
isInputPrefillEnabled,
|
||||
})
|
||||
|
||||
const handleHideQueryParamsChange = (isHideQueryParamsEnabled: boolean) =>
|
||||
onGeneralSettingsChange({
|
||||
...generalSettings,
|
||||
isHideQueryParamsEnabled,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<ChangePlanModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
type={LimitReached.BRAND}
|
||||
/>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
align="center"
|
||||
onClick={isWorkspaceFreePlan ? onOpen : undefined}
|
||||
>
|
||||
<FormLabel htmlFor="branding" mb="0">
|
||||
Typebot.io branding{' '}
|
||||
{isWorkspaceFreePlan && <LockTag plan={Plan.STARTER} />}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="branding"
|
||||
isChecked={generalSettings.isBrandingEnabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
<SwitchWithLabel
|
||||
label="Prefill input"
|
||||
initialValue={generalSettings.isInputPrefillEnabled ?? true}
|
||||
onCheckChange={handleInputPrefillChange}
|
||||
moreInfoContent="Inputs are automatically pre-filled whenever their associated variable has a value"
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Remember session"
|
||||
initialValue={
|
||||
isDefined(generalSettings.isNewResultOnRefreshEnabled)
|
||||
? !generalSettings.isNewResultOnRefreshEnabled
|
||||
: true
|
||||
}
|
||||
onCheckChange={handleNewResultOnRefreshChange}
|
||||
moreInfoContent="If the user refreshes the page, its existing results will be overwritten. Disable this if you want to create a new results every time the user refreshes the page."
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
label="Hide query params on bot start"
|
||||
initialValue={generalSettings.isHideQueryParamsEnabled ?? true}
|
||||
onCheckChange={handleHideQueryParamsChange}
|
||||
moreInfoContent="If your URL contains query params, they will be automatically hidden when the bot starts."
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
133
apps/builder/src/features/settings/components/MetadataForm.tsx
Normal file
133
apps/builder/src/features/settings/components/MetadataForm.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React from 'react'
|
||||
import { Metadata } from 'models'
|
||||
import {
|
||||
FormLabel,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Stack,
|
||||
Image,
|
||||
PopoverContent,
|
||||
HStack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { Input, Textarea } from '@/components/inputs'
|
||||
|
||||
type Props = {
|
||||
typebotId: string
|
||||
typebotName: string
|
||||
metadata: Metadata
|
||||
onMetadataChange: (metadata: Metadata) => void
|
||||
}
|
||||
|
||||
export const MetadataForm = ({
|
||||
typebotId,
|
||||
typebotName,
|
||||
metadata,
|
||||
onMetadataChange,
|
||||
}: Props) => {
|
||||
const handleTitleChange = (title: string) =>
|
||||
onMetadataChange({ ...metadata, title })
|
||||
const handleDescriptionChange = (description: string) =>
|
||||
onMetadataChange({ ...metadata, description })
|
||||
const handleFavIconSubmit = (favIconUrl: string) =>
|
||||
onMetadataChange({ ...metadata, favIconUrl })
|
||||
const handleImageSubmit = (imageUrl: string) =>
|
||||
onMetadataChange({ ...metadata, imageUrl })
|
||||
const handleHeadCodeChange = (customHeadCode: string) =>
|
||||
onMetadataChange({ ...metadata, customHeadCode })
|
||||
|
||||
return (
|
||||
<Stack spacing="6">
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="icon">
|
||||
Icon:
|
||||
</FormLabel>
|
||||
<Popover isLazy placement="top">
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.favIconUrl ?? '/favicon.png'}
|
||||
w="20px"
|
||||
alt="Fav icon"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
filePath={`typebots/${typebotId}/favIcon`}
|
||||
defaultUrl={metadata.favIconUrl ?? ''}
|
||||
onSubmit={handleFavIconSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="image">
|
||||
Image:
|
||||
</FormLabel>
|
||||
<Popover isLazy placement="top">
|
||||
<PopoverTrigger>
|
||||
<Image
|
||||
src={metadata.imageUrl ?? '/viewer-preview.png'}
|
||||
alt="Website image"
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p="4">
|
||||
<ImageUploadContent
|
||||
filePath={`typebots/${typebotId}/ogImage`}
|
||||
defaultUrl={metadata.imageUrl}
|
||||
onSubmit={handleImageSubmit}
|
||||
isGiphyEnabled={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="title">
|
||||
Title:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="title"
|
||||
defaultValue={metadata.title ?? typebotName}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="description">
|
||||
Description:
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
id="description"
|
||||
defaultValue={metadata.description}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<HStack as={FormLabel} mb="0" htmlFor="head">
|
||||
<Text>Custom head code:</Text>
|
||||
<MoreInfoTooltip>
|
||||
Will be pasted at the bottom of the header section, just above the
|
||||
closing head tag. Only `meta` and `script` tags are allowed.
|
||||
</MoreInfoTooltip>
|
||||
</HStack>
|
||||
<CodeEditor
|
||||
id="head"
|
||||
value={metadata.customHeadCode ?? ''}
|
||||
onChange={handleHeadCodeChange}
|
||||
lang="html"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { Seo } from '@/components/Seo'
|
||||
import { TypebotHeader, useTypebot } from '@/features/editor'
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useMemo } from 'react'
|
||||
import { getViewerUrl } from 'utils'
|
||||
import { SettingsSideMenu } from './SettingsSideMenu'
|
||||
import { parseTypebotToPublicTypebot } from '@/features/publish'
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const publicTypebot = useMemo(
|
||||
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[typebot?.settings]
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title="Settings" />
|
||||
<TypebotHeader />
|
||||
<Flex h="full" w="full">
|
||||
<SettingsSideMenu />
|
||||
<Flex flex="1">
|
||||
{publicTypebot && (
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
typebot={publicTypebot}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Heading,
|
||||
HStack,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, MoreVerticalIcon } from '@/components/icons'
|
||||
import { GeneralSettings, Metadata, TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { GeneralSettingsForm } from './GeneralSettingsForm'
|
||||
import { MetadataForm } from './MetadataForm'
|
||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||
import { headerHeight, useTypebot } from '@/features/editor'
|
||||
|
||||
export const SettingsSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleTypingEmulationChange = (typingEmulation: TypingEmulation) =>
|
||||
typebot &&
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
|
||||
const handleGeneralSettingsChange = (general: GeneralSettings) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, general } })
|
||||
|
||||
const handleMetadataChange = (metadata: Metadata) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, metadata } })
|
||||
|
||||
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">
|
||||
Settings
|
||||
</Heading>
|
||||
<Accordion allowMultiple defaultIndex={[0]}>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<MoreVerticalIcon transform={'rotate(90deg)'} />
|
||||
<Heading fontSize="lg">General</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<GeneralSettingsForm
|
||||
generalSettings={typebot.settings.general}
|
||||
onGeneralSettingsChange={handleGeneralSettingsChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<ChatIcon />
|
||||
<Heading fontSize="lg">Typing emulation</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<TypingEmulationForm
|
||||
typingEmulation={typebot.settings.typingEmulation}
|
||||
onUpdate={handleTypingEmulationChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<CodeIcon />
|
||||
<Heading fontSize="lg">Metadata</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px="6">
|
||||
{typebot && (
|
||||
<MetadataForm
|
||||
typebotId={typebot.id}
|
||||
typebotName={typebot.name}
|
||||
metadata={typebot.settings.metadata}
|
||||
onMetadataChange={handleMetadataChange}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
|
||||
import { TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { SmartNumberInput } from '@/components/inputs'
|
||||
|
||||
type Props = {
|
||||
typingEmulation: TypingEmulation
|
||||
onUpdate: (typingEmulation: TypingEmulation) => void
|
||||
}
|
||||
|
||||
export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
|
||||
const handleSwitchChange = () =>
|
||||
onUpdate({
|
||||
...typingEmulation,
|
||||
enabled: !typingEmulation.enabled,
|
||||
})
|
||||
|
||||
const handleSpeedChange = (speed?: number) =>
|
||||
isDefined(speed) && onUpdate({ ...typingEmulation, speed })
|
||||
|
||||
const handleMaxDelayChange = (maxDelay?: number) =>
|
||||
isDefined(maxDelay) && onUpdate({ ...typingEmulation, maxDelay })
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Flex justifyContent="space-between" align="center">
|
||||
<FormLabel htmlFor="typing-emulation" mb="0">
|
||||
Typing emulation
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="typing-emulation"
|
||||
isChecked={typingEmulation.enabled}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</Flex>
|
||||
{typingEmulation.enabled && (
|
||||
<Stack pl={10}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="speed" mb="0">
|
||||
Words per minutes:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="speed"
|
||||
data-testid="speed"
|
||||
value={typingEmulation.speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
maxW="100px"
|
||||
step={30}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<FormLabel htmlFor="max-delay" mb="0">
|
||||
Max delay (in seconds):
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="max-delay"
|
||||
data-testid="max-delay"
|
||||
value={typingEmulation.maxDelay}
|
||||
onValueChange={handleMaxDelayChange}
|
||||
maxW="100px"
|
||||
step={0.1}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
1
apps/builder/src/features/settings/index.ts
Normal file
1
apps/builder/src/features/settings/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { SettingsPage } from './components/SettingsPage'
|
135
apps/builder/src/features/settings/settings.spec.ts
Normal file
135
apps/builder/src/features/settings/settings.spec.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import test, { expect } from '@playwright/test'
|
||||
import cuid from 'cuid'
|
||||
import { defaultTextInputOptions } from 'models'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { freeWorkspaceId } from 'utils/playwright/databaseSetup'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
test.describe.parallel('Settings page', () => {
|
||||
test.describe('General', () => {
|
||||
test('should reflect change in real-time', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/settings.json'), {
|
||||
id: typebotId,
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/settings`)
|
||||
await expect(
|
||||
typebotViewer(page).locator('a:has-text("Made with Typebot")')
|
||||
).toHaveAttribute('href', 'https://www.typebot.io/?utm_source=litebadge')
|
||||
await page.click('text="Typebot.io branding"')
|
||||
await expect(
|
||||
typebotViewer(page).locator('a:has-text("Made with Typebot")')
|
||||
).toBeHidden()
|
||||
|
||||
await page.click('text="Remember session"')
|
||||
await expect(
|
||||
page.locator('input[type="checkbox"] >> nth=-1')
|
||||
).toHaveAttribute('checked', '')
|
||||
|
||||
await expect(
|
||||
typebotViewer(page).locator('input[value="Baptiste"]')
|
||||
).toBeVisible()
|
||||
await page.click('text=Prefill input')
|
||||
await page.click('text=Theme')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Typing emulation', () => {
|
||||
test('should be fillable', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/settings.json'), {
|
||||
id: typebotId,
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/settings`)
|
||||
await expect(
|
||||
typebotViewer(page).locator('a:has-text("Made with Typebot")')
|
||||
).toHaveAttribute('href', 'https://www.typebot.io/?utm_source=litebadge')
|
||||
await page.click('button:has-text("Typing emulation")')
|
||||
await page.fill('[data-testid="speed"] input', '350')
|
||||
await page.fill('[data-testid="max-delay"] input', '1.5')
|
||||
await page.click('text="Typing emulation" >> nth=-1')
|
||||
await expect(page.locator('[data-testid="speed"]')).toBeHidden()
|
||||
await expect(page.locator('[data-testid="max-delay"]')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Metadata', () => {
|
||||
test('should be fillable', async ({ page }) => {
|
||||
const favIconUrl = 'https://www.baptistearno.com/favicon.png'
|
||||
const imageUrl = 'https://www.baptistearno.com/images/site-preview.png'
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/settings.json'), {
|
||||
id: typebotId,
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/settings`)
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveValue('Baptiste')
|
||||
await page.click('button:has-text("Metadata")')
|
||||
|
||||
// Fav icon
|
||||
const favIconImg = page.locator('img >> nth=0')
|
||||
await expect(favIconImg).toHaveAttribute('src', '/favicon.png')
|
||||
await favIconImg.click()
|
||||
await expect(page.locator('text=Giphy')).toBeHidden()
|
||||
await page.click('button:has-text("Embed link")')
|
||||
await page.fill(
|
||||
'input[placeholder="Paste the image link..."]',
|
||||
favIconUrl
|
||||
)
|
||||
await expect(favIconImg).toHaveAttribute('src', favIconUrl)
|
||||
|
||||
// Website image
|
||||
const websiteImg = page.locator('img >> nth=1')
|
||||
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
|
||||
await websiteImg.click({ position: { x: 0, y: 160 }, force: true })
|
||||
await expect(page.locator('text=Giphy')).toBeHidden()
|
||||
await page.click('button >> text="Embed link"')
|
||||
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
||||
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
||||
|
||||
// Title
|
||||
await page.fill('input#title', 'Awesome typebot')
|
||||
|
||||
// Description
|
||||
await page.fill('textarea#description', 'Lorem ipsum')
|
||||
|
||||
// Custom head code
|
||||
await page.fill(
|
||||
'div[contenteditable=true]',
|
||||
'<script>Lorem ipsum</script>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Free workspace', () => {
|
||||
test("can't remove branding", async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(getTestAsset('typebots/settings.json'), {
|
||||
id: typebotId,
|
||||
workspaceId: freeWorkspaceId,
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/settings`)
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="What\'s your name?"')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.locator('[data-testid="starter-lock-tag"]')
|
||||
).toBeVisible()
|
||||
await page.click('text=Typebot.io branding')
|
||||
await expect(
|
||||
page.locator(
|
||||
'text="You need to upgrade your plan in order to remove branding"'
|
||||
)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user