perf(e2e): ⚡️ Migrate to Playwright
This commit is contained in:
@ -37,3 +37,6 @@ STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# (Optional) Used for GIF search
|
||||
NEXT_PUBLIC_GIPHY_API_KEY=
|
||||
|
||||
# (Optional) for e2e tests
|
||||
GOOGLE_REFRESH_TOKEN_TEST=
|
||||
|
26
apps/builder/.github/workflows/playwright.yml
vendored
Normal file
26
apps/builder/.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-test-results
|
||||
path: test-results/
|
9
apps/builder/.gitignore
vendored
9
apps/builder/.gitignore
vendored
@ -1,3 +1,10 @@
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/downloads
|
||||
cypress/downloads
|
||||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
authenticatedState.json
|
||||
|
||||
.env
|
@ -286,3 +286,14 @@ export const WebhookIcon = (props: IconProps) => (
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const GripIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<circle cx="12" cy="9" r="1"></circle>
|
||||
<circle cx="19" cy="9" r="1"></circle>
|
||||
<circle cx="5" cy="9" r="1"></circle>
|
||||
<circle cx="12" cy="15" r="1"></circle>
|
||||
<circle cx="19" cy="15" r="1"></circle>
|
||||
<circle cx="5" cy="15" r="1"></circle>
|
||||
</Icon>
|
||||
)
|
||||
|
3
apps/builder/assets/styles/custom.css
Normal file
3
apps/builder/assets/styles/custom.css
Normal file
@ -0,0 +1,3 @@
|
||||
.grabbing * {
|
||||
cursor: grabbing !important;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import Graph from './graph/Graph'
|
||||
import { DndContext } from 'contexts/DndContext'
|
||||
import { StepDndContext } from 'contexts/StepDndContext'
|
||||
import { StepTypesList } from './StepTypesList'
|
||||
import { PreviewDrawer } from './preview/PreviewDrawer'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
@ -12,14 +12,14 @@ export const Board = () => {
|
||||
const { rightPanel } = useEditor()
|
||||
return (
|
||||
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
|
||||
<DndContext>
|
||||
<StepDndContext>
|
||||
<StepTypesList />
|
||||
<GraphProvider>
|
||||
<Graph flex="1" />
|
||||
<BoardMenuButton pos="absolute" right="40px" top="20px" />
|
||||
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
||||
</GraphProvider>
|
||||
</DndContext>
|
||||
</StepDndContext>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Flex, HStack, StackProps, Text } from '@chakra-ui/react'
|
||||
import { StepType, DraggableStepType } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
import { StepTypeLabel } from './StepTypeLabel'
|
||||
@ -12,7 +12,7 @@ export const StepCard = ({
|
||||
type: DraggableStepType
|
||||
onMouseDown: (e: React.MouseEvent, type: DraggableStepType) => void
|
||||
}) => {
|
||||
const { draggedStepType } = useDnd()
|
||||
const { draggedStepType } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -13,12 +13,12 @@ import {
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
} from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
|
||||
export const StepTypesList = () => {
|
||||
const { setDraggedStepType, draggedStepType } = useDnd()
|
||||
const { setDraggedStepType, draggedStepType } = useStepDnd()
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Block } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { StepsList } from './StepsList'
|
||||
import { filterTable, isDefined } from 'utils'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
@ -22,8 +22,8 @@ type Props = {
|
||||
export const BlockNode = ({ block }: Props) => {
|
||||
const { connectingIds, setConnectingIds, previewingEdgeId } = useGraph()
|
||||
const { typebot, updateBlock } = useTypebot()
|
||||
const { setMouseOverBlockId } = useDnd()
|
||||
const { draggedStep, draggedStepType } = useDnd()
|
||||
const { setMouseOverBlockId } = useStepDnd()
|
||||
const { draggedStep, draggedStepType } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const isPreviewing = useMemo(() => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ChoiceInputStep, ChoiceItem } from 'models'
|
||||
@ -19,7 +19,7 @@ export const ChoiceItemsList = ({ step }: ChoiceItemsListProps) => {
|
||||
mouseOverBlockId,
|
||||
setDraggedChoiceItem,
|
||||
setMouseOverBlockId,
|
||||
} = useDnd()
|
||||
} = useStepDnd()
|
||||
const showSortPlaceholders = useMemo(
|
||||
() => mouseOverBlockId === step.blockId && draggedChoiceItem,
|
||||
[draggedChoiceItem, mouseOverBlockId, step.blockId]
|
||||
|
@ -28,7 +28,6 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
placeholder="Paste the video link..."
|
||||
initialValue={content?.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
delay={100}
|
||||
/>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
Works with Youtube, Vimeo and others
|
||||
|
@ -31,13 +31,12 @@ export const ChoiceInputSettingsBody = ({
|
||||
/>
|
||||
{options?.isMultipleChoice && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="send">
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="send"
|
||||
id="button"
|
||||
initialValue={options?.buttonLabel ?? 'Send'}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -40,7 +40,6 @@ export const ComparisonItem = ({
|
||||
/>
|
||||
{item.comparisonOperator !== ComparisonOperators.IS_SET && (
|
||||
<InputWithVariableButton
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleChangeValue}
|
||||
placeholder="Type a value..."
|
||||
|
@ -21,6 +21,7 @@ export const ConditionSettingsBody = ({
|
||||
|
||||
return (
|
||||
<TableList<Comparison>
|
||||
initialItems={options.comparisons}
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
|
@ -49,7 +49,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="from"
|
||||
initialValue={options.labels.from}
|
||||
delay={100}
|
||||
onChange={handleFromChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -62,7 +61,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="to"
|
||||
initialValue={options.labels.to}
|
||||
delay={100}
|
||||
onChange={handleToChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -74,7 +72,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const EmailInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const EmailInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -51,7 +51,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="tracking-id"
|
||||
initialValue={options?.trackingId ?? ''}
|
||||
placeholder="G-123456..."
|
||||
delay={100}
|
||||
onChange={handleTrackingIdChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -63,7 +62,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="category"
|
||||
initialValue={options?.category ?? ''}
|
||||
placeholder="Example: Typebot"
|
||||
delay={100}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -75,7 +73,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="action"
|
||||
initialValue={options?.action ?? ''}
|
||||
placeholder="Example: Submit email"
|
||||
delay={100}
|
||||
onChange={handleActionChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -98,7 +95,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="label"
|
||||
initialValue={options?.label ?? ''}
|
||||
placeholder="Example: Campaign Z"
|
||||
delay={100}
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -149,7 +149,7 @@ const ActionOptions = ({
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return (
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToInsert}
|
||||
initialItems={options.cellsToInsert ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleInsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
@ -167,7 +167,7 @@ const ActionOptions = ({
|
||||
/>
|
||||
<Text>Cells to update</Text>
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToUpsert}
|
||||
initialItems={options.cellsToUpsert ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleUpsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
@ -186,7 +186,7 @@ const ActionOptions = ({
|
||||
/>
|
||||
<Text>Cells to extract</Text>
|
||||
<TableList<ExtractingCell>
|
||||
initialItems={options.cellsToExtract}
|
||||
initialItems={options.cellsToExtract ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleExtractingCellsChange}
|
||||
Item={ExtractingCellItem}
|
||||
addLabel="Add a value"
|
||||
|
@ -30,8 +30,8 @@ export const SheetsDropdown = ({
|
||||
selectedItem={currentSheet?.name}
|
||||
items={(sheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Select the sheet'}
|
||||
isDisabled={isLoading}
|
||||
placeholder={'Select the sheet'}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ export const SpreadsheetsDropdown = ({
|
||||
selectedItem={currentSpreadsheet?.name}
|
||||
items={(spreadsheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Search for spreadsheet'}
|
||||
isDisabled={isLoading}
|
||||
placeholder={'Search for spreadsheet'}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ export const NumberInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -48,7 +47,6 @@ export const NumberInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const PhoneNumberSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const PhoneNumberSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
|
||||
import { RedirectOptions } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
@ -21,11 +22,10 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<FormLabel mb="0" htmlFor="tracking-id">
|
||||
Url:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
<InputWithVariableButton
|
||||
id="tracking-id"
|
||||
initialValue={options.url ?? ''}
|
||||
placeholder="Type a URL..."
|
||||
delay={100}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -37,7 +37,6 @@ export const SetVariableSettingsBody = ({
|
||||
<DebouncedTextarea
|
||||
id="expression"
|
||||
initialValue={options.expressionToEvaluate ?? ''}
|
||||
delay={100}
|
||||
onChange={handleExpressionChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -38,7 +38,6 @@ export const TextInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -49,7 +48,6 @@ export const TextInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const UrlInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const UrlInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const VariableForTestInputs = ({
|
||||
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel>
|
||||
<DebouncedInput
|
||||
id={'value' + id}
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
|
@ -130,7 +130,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.queryParams}
|
||||
initialItems={webhook?.queryParams ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
@ -144,7 +144,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.headers}
|
||||
initialItems={webhook?.headers ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
@ -171,7 +171,9 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={options?.variablesForTest}
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
@ -194,7 +196,9 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options?.responseVariableMapping}
|
||||
initialItems={
|
||||
options?.responseVariableMapping ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
/>
|
||||
|
@ -8,10 +8,9 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BubbleStep, DraggableStep, Step, TextBubbleStep } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import { isBubbleStep, isTextBubbleStep } from 'utils'
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||
import { TextEditor } from './TextEditor/TextEditor'
|
||||
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
@ -43,7 +42,8 @@ export const StepNode = ({
|
||||
) => void
|
||||
}) => {
|
||||
const { query } = useRouter()
|
||||
const { setConnectingIds, connectingIds } = useGraph()
|
||||
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
|
||||
useGraph()
|
||||
const { moveStep } = useTypebot()
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [mouseDownEvent, setMouseDownEvent] =
|
||||
@ -57,6 +57,11 @@ export const StepNode = ({
|
||||
onClose: onModalClose,
|
||||
} = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
if (query.stepId?.toString() === step.id) setOpenedStepId(step.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnecting(
|
||||
connectingIds?.target?.blockId === step.blockId &&
|
||||
@ -126,6 +131,16 @@ export const StepNode = ({
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpenedStepId(step.id)
|
||||
}
|
||||
|
||||
const handleExpandClick = () => {
|
||||
setOpenedStepId(undefined)
|
||||
onModalOpen()
|
||||
}
|
||||
|
||||
return isEditing && isTextBubbleStep(step) ? (
|
||||
<TextEditor
|
||||
stepId={step.id}
|
||||
@ -137,11 +152,7 @@ export const StepNode = ({
|
||||
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Popover
|
||||
placement="left"
|
||||
isLazy
|
||||
defaultIsOpen={query.stepId?.toString() === step.id}
|
||||
>
|
||||
<Popover placement="left" isLazy isOpen={openedStepId === step.id}>
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
pos="relative"
|
||||
@ -151,6 +162,7 @@ export const StepNode = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
data-testid={`step-${step.id}`}
|
||||
w="full"
|
||||
>
|
||||
@ -166,7 +178,11 @@ export const StepNode = ({
|
||||
align="flex-start"
|
||||
w="full"
|
||||
>
|
||||
<StepIcon type={step.type} mt="1" />
|
||||
<StepIcon
|
||||
type={step.type}
|
||||
mt="1"
|
||||
data-testid={`${step.id}-icon`}
|
||||
/>
|
||||
<StepNodeContent step={step} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
@ -189,7 +205,10 @@ export const StepNode = ({
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(step) && (
|
||||
<SettingsPopoverContent step={step} onExpandClick={onModalOpen} />
|
||||
<SettingsPopoverContent
|
||||
step={step}
|
||||
onExpandClick={handleExpandClick}
|
||||
/>
|
||||
)}
|
||||
{hasContentPopover(step) && <ContentPopover step={step} />}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={onModalClose}>
|
||||
|
@ -7,26 +7,31 @@ export const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Flex>
|
||||
<Stack color={'gray.500'}>
|
||||
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
const variable = typebot?.variables.byId[comparison?.variableId ?? '']
|
||||
return (
|
||||
<HStack key={comparisonId} spacing={1}>
|
||||
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400">{variable.name}</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>{comparison?.comparisonOperator}</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
{step.options?.comparisons.allIds.length === 0 ? (
|
||||
<Text color={'gray.500'}>Configure...</Text>
|
||||
) : (
|
||||
<Stack>
|
||||
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
const variable =
|
||||
typebot?.variables.byId[comparison?.variableId ?? '']
|
||||
return (
|
||||
<HStack key={comparisonId} spacing={1}>
|
||||
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400">{variable.name}</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>{comparison?.comparisonOperator}</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
|
@ -80,11 +80,7 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
)
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.from ?? 'Pick a date...'}
|
||||
</Text>
|
||||
)
|
||||
return <Text color={'gray.500'}>Pick a date...</Text>
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||
import { DraggableStep, Step, Table } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { StepNode, StepNodeOverlay } from './StepNode'
|
||||
@ -20,7 +20,7 @@ export const StepsList = ({
|
||||
mouseOverBlockId,
|
||||
setDraggedStepType,
|
||||
setMouseOverBlockId,
|
||||
} = useDnd()
|
||||
} = useStepDnd()
|
||||
const { createStep } = useTypebot()
|
||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||
number | undefined
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo } from 'react'
|
||||
import React, { useRef, useMemo, useEffect } from 'react'
|
||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||
import { BlockNode } from './BlockNode/BlockNode'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Edges } from './Edges'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
@ -10,16 +10,23 @@ import { DraggableStepType } from 'models'
|
||||
|
||||
const Graph = ({ ...props }: FlexProps) => {
|
||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||
useDnd()
|
||||
useStepDnd()
|
||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const { createBlock, typebot } = useTypebot()
|
||||
const { graphPosition, setGraphPosition } = useGraph()
|
||||
const { graphPosition, setGraphPosition, setOpenedStepId } = useGraph()
|
||||
const transform = useMemo(
|
||||
() =>
|
||||
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
|
||||
[graphPosition]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
editorContainerRef.current = document.getElementById(
|
||||
'editor-container'
|
||||
) as HTMLDivElement
|
||||
}, [])
|
||||
|
||||
const handleMouseWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
const isPinchingTrackpad = e.ctrlKey
|
||||
@ -57,6 +64,9 @@ const Graph = ({ ...props }: FlexProps) => {
|
||||
}
|
||||
useEventListener('mousedown', handleMouseDown, undefined, { capture: true })
|
||||
|
||||
const handleClick = () => setOpenedStepId(undefined)
|
||||
useEventListener('click', handleClick, editorContainerRef.current)
|
||||
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex ref={graphContainerRef} {...props}>
|
||||
|
@ -1,26 +1,20 @@
|
||||
import { DashboardFolder } from '.prisma/client'
|
||||
import { Typebot } from 'models'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Portal,
|
||||
Skeleton,
|
||||
Stack,
|
||||
useEventListener,
|
||||
useToast,
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { FolderPlusIcon } from 'assets/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import { Typebot } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createFolder, useFolders } from 'services/folders'
|
||||
import { patchTypebot, useTypebots } from 'services/typebots'
|
||||
import { BackButton } from './FolderContent/BackButton'
|
||||
@ -31,16 +25,24 @@ import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
const dragDistanceTolerance = 20
|
||||
|
||||
export const FolderContent = ({ folder }: Props) => {
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||
const [draggedTypebot, setDraggedTypebot] = useState<Typebot | undefined>()
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 20,
|
||||
},
|
||||
})
|
||||
)
|
||||
const {
|
||||
setDraggedTypebot,
|
||||
draggedTypebot,
|
||||
mouseOverFolderId,
|
||||
setMouseOverFolderId,
|
||||
} = useTypebotDnd()
|
||||
const [mouseDownPosition, setMouseDownPosition] = useState({ x: 0, y: 0 })
|
||||
const [draggablePosition, setDraggablePosition] = useState({ x: 0, y: 0 })
|
||||
const [relativeDraggablePosition, setRelativeDraggablePosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const [typebotDragCandidate, setTypebotDragCandidate] = useState<Typebot>()
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
@ -67,19 +69,6 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
},
|
||||
})
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
if (!typebots) return
|
||||
setDraggedTypebot(typebots.find((c) => c.id === event.active.id))
|
||||
}
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
if (!typebots) return
|
||||
const { over } = event
|
||||
if (over?.id && draggedTypebot?.id)
|
||||
await moveTypebotToFolder(draggedTypebot.id, over.id)
|
||||
setDraggedTypebot(undefined)
|
||||
}
|
||||
|
||||
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
|
||||
if (!typebots) return
|
||||
const { error } = await patchTypebot(typebotId, {
|
||||
@ -118,63 +107,103 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseUp = async () => {
|
||||
if (mouseOverFolderId !== undefined && draggedTypebot)
|
||||
await moveTypebotToFolder(draggedTypebot.id, mouseOverFolderId ?? 'root')
|
||||
setTypebotDragCandidate(undefined)
|
||||
setMouseOverFolderId(undefined)
|
||||
setDraggedTypebot(undefined)
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp)
|
||||
|
||||
const handleMouseDown = (typebot: Typebot) => (e: React.MouseEvent) => {
|
||||
const element = e.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
setDraggablePosition({ x: rect.left, y: rect.top })
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
setRelativeDraggablePosition({ x, y })
|
||||
setMouseDownPosition({ x: e.screenX, y: e.screenY })
|
||||
setTypebotDragCandidate(typebot)
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!typebotDragCandidate) return
|
||||
const { clientX, clientY, screenX, screenY } = e
|
||||
if (
|
||||
Math.abs(mouseDownPosition.x - screenX) > dragDistanceTolerance ||
|
||||
Math.abs(mouseDownPosition.y - screenY) > dragDistanceTolerance
|
||||
)
|
||||
setDraggedTypebot(typebotDragCandidate)
|
||||
setDraggablePosition({
|
||||
...draggablePosition,
|
||||
x: clientX - relativeDraggablePosition.x,
|
||||
y: clientY - relativeDraggablePosition.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return (
|
||||
<Flex w="full" justify="center" align="center" pt={4}>
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
</Skeleton>
|
||||
<Stack>
|
||||
<HStack>
|
||||
{folder && <BackButton id={folder.parentFolderId} />}
|
||||
<Button
|
||||
leftIcon={<FolderPlusIcon />}
|
||||
onClick={handleCreateFolder}
|
||||
isLoading={isCreatingFolder || isFolderLoading}
|
||||
>
|
||||
Create a folder
|
||||
</Button>
|
||||
</HStack>
|
||||
<Wrap spacing={4}>
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
/>
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
{folders &&
|
||||
folders.map((folder) => (
|
||||
<FolderButton
|
||||
key={folder.id.toString()}
|
||||
folder={folder}
|
||||
onFolderDeleted={() => handleFolderDeleted(folder.id)}
|
||||
onFolderRenamed={(newName: string) =>
|
||||
handleFolderRenamed(folder.id, newName)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{isTypebotLoading && <ButtonSkeleton />}
|
||||
{typebots &&
|
||||
typebots.map((typebot) => (
|
||||
<TypebotButton
|
||||
key={typebot.id.toString()}
|
||||
typebot={typebot}
|
||||
onTypebotDeleted={() => handleTypebotDeleted(typebot.id)}
|
||||
/>
|
||||
))}
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{draggedTypebot && (
|
||||
<TypebotCardOverlay typebot={draggedTypebot} />
|
||||
)}
|
||||
</DragOverlay>
|
||||
</Wrap>
|
||||
</Stack>
|
||||
<Flex w="full" flex="1" justify="center" pt={4}>
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
</Skeleton>
|
||||
<Stack>
|
||||
<HStack>
|
||||
{folder && <BackButton id={folder.parentFolderId} />}
|
||||
<Button
|
||||
leftIcon={<FolderPlusIcon />}
|
||||
onClick={handleCreateFolder}
|
||||
isLoading={isCreatingFolder || isFolderLoading}
|
||||
>
|
||||
Create a folder
|
||||
</Button>
|
||||
</HStack>
|
||||
<Wrap spacing={4}>
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
/>
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
{folders &&
|
||||
folders.map((folder) => (
|
||||
<FolderButton
|
||||
key={folder.id.toString()}
|
||||
folder={folder}
|
||||
onFolderDeleted={() => handleFolderDeleted(folder.id)}
|
||||
onFolderRenamed={(newName: string) =>
|
||||
handleFolderRenamed(folder.id, newName)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{isTypebotLoading && <ButtonSkeleton />}
|
||||
{typebots &&
|
||||
typebots.map((typebot) => (
|
||||
<TypebotButton
|
||||
key={typebot.id.toString()}
|
||||
typebot={typebot}
|
||||
onTypebotDeleted={() => handleTypebotDeleted(typebot.id)}
|
||||
onMouseDown={handleMouseDown(typebot)}
|
||||
/>
|
||||
))}
|
||||
</Wrap>
|
||||
</Stack>
|
||||
</DndContext>
|
||||
</Stack>
|
||||
{draggedTypebot && (
|
||||
<Portal>
|
||||
<TypebotCardOverlay
|
||||
typebot={draggedTypebot}
|
||||
onMouseUp={handleMouseUp}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -1,22 +1,30 @@
|
||||
import { Button } from '@chakra-ui/react'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import React from 'react'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
export const BackButton = ({ id }: { id: string | null }) => {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: id?.toString() ?? 'root',
|
||||
})
|
||||
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
|
||||
useTypebotDnd()
|
||||
|
||||
const isTypebotOver = useMemo(
|
||||
() => draggedTypebot && mouseOverFolderId === id,
|
||||
[draggedTypebot, id, mouseOverFolderId]
|
||||
)
|
||||
|
||||
const handleMouseEnter = () => setMouseOverFolderId(id)
|
||||
const handleMouseLeave = () => setMouseOverFolderId(undefined)
|
||||
return (
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={id ? `/typebots/folders/${id}` : '/typebots'}
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
variant={'outline'}
|
||||
colorScheme={isOver ? 'blue' : 'gray'}
|
||||
borderWidth={isOver ? '3px' : '1px'}
|
||||
ref={setNodeRef}
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
@ -17,11 +17,11 @@ import {
|
||||
SkeletonCircle,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { FolderIcon, MoreVerticalIcon } from 'assets/icons'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { deleteFolder, updateFolder } from 'services/folders'
|
||||
|
||||
export const FolderButton = ({
|
||||
@ -34,9 +34,12 @@ export const FolderButton = ({
|
||||
onFolderRenamed: (newName: string) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: folder.id.toString(),
|
||||
})
|
||||
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
|
||||
useTypebotDnd()
|
||||
const isTypebotOver = useMemo(
|
||||
() => draggedTypebot && mouseOverFolderId === folder.id,
|
||||
[draggedTypebot, folder.id, mouseOverFolderId]
|
||||
)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
@ -65,31 +68,33 @@ export const FolderButton = ({
|
||||
router.push(`/typebots/folders/${folder.id}`)
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => setMouseOverFolderId(folder.id)
|
||||
const handleMouseLeave = () => setMouseOverFolderId(undefined)
|
||||
return (
|
||||
<Button
|
||||
as={WrapItem}
|
||||
ref={setNodeRef}
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
paddingX={6}
|
||||
whiteSpace={'normal'}
|
||||
pos="relative"
|
||||
cursor="pointer"
|
||||
variant="outline"
|
||||
colorScheme={isOver ? 'blue' : 'gray'}
|
||||
borderWidth={isOver ? '3px' : '1px'}
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
justifyContent="center"
|
||||
onClick={handleClick}
|
||||
data-testid="folder-button"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<MoreVerticalIcon />}
|
||||
aria-label="Show folder menu"
|
||||
aria-label={`Show ${folder.name} menu`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
colorScheme="gray"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top="5"
|
||||
right="5"
|
||||
|
@ -16,10 +16,11 @@ export const MoreButton = ({ children, ...props }: Props) => {
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<MoreVerticalIcon />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
onMouseUp={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
colorScheme="gray"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
<MenuList>{children}</MenuList>
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Text,
|
||||
useDisclosure,
|
||||
@ -9,28 +10,28 @@ import {
|
||||
VStack,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { useRouter } from 'next/router'
|
||||
import { isMobile } from 'services/utils'
|
||||
import { MoreButton } from 'components/MoreButton'
|
||||
import { MoreButton } from 'components/dashboard/FolderContent/MoreButton'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { GlobeIcon, ToolIcon } from 'assets/icons'
|
||||
import { GlobeIcon, GripIcon, ToolIcon } from 'assets/icons'
|
||||
import { deleteTypebot, duplicateTypebot } from 'services/typebots'
|
||||
import { Typebot } from 'models'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
|
||||
type ChatbotCardProps = {
|
||||
typebot: Typebot
|
||||
onTypebotDeleted: () => void
|
||||
onMouseDown: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
export const TypebotButton = ({
|
||||
typebot,
|
||||
onTypebotDeleted,
|
||||
onMouseDown,
|
||||
}: ChatbotCardProps) => {
|
||||
const router = useRouter()
|
||||
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
||||
id: typebot.id.toString(),
|
||||
})
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
const {
|
||||
isOpen: isDeleteOpen,
|
||||
onOpen: onDeleteOpen,
|
||||
@ -43,6 +44,7 @@ export const TypebotButton = ({
|
||||
})
|
||||
|
||||
const handleTypebotClick = () => {
|
||||
if (draggedTypebot) return
|
||||
router.push(
|
||||
isMobile
|
||||
? `/typebots/${typebot.id}/results/responses`
|
||||
@ -73,7 +75,7 @@ export const TypebotButton = ({
|
||||
return (
|
||||
<Button
|
||||
as={WrapItem}
|
||||
onClick={handleTypebotClick}
|
||||
onMouseUp={handleTypebotClick}
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
@ -84,17 +86,26 @@ export const TypebotButton = ({
|
||||
mb={6}
|
||||
rounded="lg"
|
||||
whiteSpace="normal"
|
||||
data-testid={`typebot-button-${typebot.id}`}
|
||||
opacity={isDragging ? 0.2 : 1}
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
opacity={draggedTypebot?.id === typebot.id ? 0.2 : 1}
|
||||
onMouseDown={onMouseDown}
|
||||
cursor="pointer"
|
||||
>
|
||||
<IconButton
|
||||
icon={<GripIcon />}
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
left="20px"
|
||||
aria-label="Drag"
|
||||
cursor="grab"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
/>
|
||||
<MoreButton
|
||||
pos="absolute"
|
||||
top="10px"
|
||||
right="10px"
|
||||
aria-label="Show typebot menu"
|
||||
top="20px"
|
||||
right="20px"
|
||||
aria-label={`Show ${typebot.name} menu`}
|
||||
>
|
||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||
<MenuItem
|
||||
|
@ -1,43 +1,47 @@
|
||||
import { Button, Flex, Text, VStack } from '@chakra-ui/react'
|
||||
import { Box, BoxProps, Flex, Text, VStack } from '@chakra-ui/react'
|
||||
import { GlobeIcon, ToolIcon } from 'assets/icons'
|
||||
import { Typebot } from 'models'
|
||||
|
||||
type Props = {
|
||||
typebot: Typebot
|
||||
}
|
||||
} & BoxProps
|
||||
|
||||
export const TypebotCardOverlay = ({ typebot }: Props) => {
|
||||
export const TypebotCardOverlay = ({ typebot, ...props }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className="sm:mr-6 mb-6 focus:outline-none rounded-lg "
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
justifyContent="center"
|
||||
w="225px"
|
||||
h="270px"
|
||||
whiteSpace="normal"
|
||||
transition="none"
|
||||
pointerEvents="none"
|
||||
borderWidth={1}
|
||||
rounded="md"
|
||||
bgColor="white"
|
||||
shadow="lg"
|
||||
opacity={0.7}
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
w="full"
|
||||
h="full"
|
||||
whiteSpace="normal"
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
rounded="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
|
||||
color="white"
|
||||
>
|
||||
{typebot.publishedTypebotId ? (
|
||||
<GlobeIcon fill="white" fontSize="20px" />
|
||||
) : (
|
||||
<ToolIcon fill="white" fontSize="20px" />
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{typebot.name}</Text>
|
||||
</VStack>
|
||||
</Button>
|
||||
</div>
|
||||
<VStack spacing={4}>
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
rounded="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
|
||||
color="white"
|
||||
>
|
||||
{typebot.publishedTypebotId ? (
|
||||
<GlobeIcon fill="white" fontSize="20px" />
|
||||
) : (
|
||||
<ToolIcon fill="white" fontSize="20px" />
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{typebot.name}</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -92,7 +92,6 @@ export const MetadataForm = ({
|
||||
<InputWithVariableButton
|
||||
id="title"
|
||||
initialValue={metadata.title ?? typebotName}
|
||||
delay={100}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -103,7 +102,6 @@ export const MetadataForm = ({
|
||||
<TextareaWithVariableButton
|
||||
id="description"
|
||||
initialValue={metadata.description}
|
||||
delay={100}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -6,27 +6,24 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedInput = forwardRef(
|
||||
(
|
||||
{ delay, onChange, initialValue, ...props }: Props,
|
||||
{ onChange, initialValue, ...props }: Props,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
}, [currentValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
|
@ -1,27 +1,23 @@
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<TextareaProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedTextarea = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
}, [currentValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
MenuButtonProps,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
@ -41,22 +42,24 @@ export const DropdownList = <T,>({
|
||||
>
|
||||
{currentItem ?? placeholder}
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px" shadow="lg">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
<Portal>
|
||||
<MenuList maxW="500px" shadow="lg" zIndex={1500}>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
@ -17,12 +17,14 @@ type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onValueChange?: (value: string) => void
|
||||
isLoading?: boolean
|
||||
} & InputProps
|
||||
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onValueChange,
|
||||
isLoading = false,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
@ -47,6 +49,7 @@ export const SearchableDropdown = ({
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
if (inputRef.current === document.activeElement) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items])
|
||||
|
||||
@ -97,6 +100,7 @@ export const SearchableDropdown = ({
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
type="text"
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
@ -130,7 +134,9 @@ export const SearchableDropdown = ({
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Text p={4}>Not found.</Text>
|
||||
<Text p={4} color="gray.500">
|
||||
{isLoading ? 'Loading...' : 'Not found.'}
|
||||
</Text>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
@ -13,7 +13,7 @@ export type TableListItemProps<T> = {
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems?: Table<T>
|
||||
initialItems: Table<T>
|
||||
onItemsChange: (items: Table<T>) => void
|
||||
addLabel?: string
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
@ -27,7 +27,7 @@ export const TableList = <T,>({
|
||||
Item,
|
||||
ComponentBetweenItems = () => <></>,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useImmer(initialItems ?? { byId: {}, allIds: [] })
|
||||
const [items, setItems] = useImmer(initialItems)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
@ -80,6 +80,7 @@ export const TableList = <T,>({
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
mt={idx !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
>
|
||||
<Item
|
||||
id={itemId}
|
||||
|
@ -13,13 +13,11 @@ import {
|
||||
import { UserIcon } from 'assets/icons'
|
||||
import { Variable } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { VariableSearchInput } from '../VariableSearchInput'
|
||||
|
||||
export type TextBoxWithVariableButtonProps = {
|
||||
initialValue: string
|
||||
onChange: (value: string) => void
|
||||
delay?: number
|
||||
TextBox:
|
||||
| ComponentWithAs<'textarea', TextareaProps>
|
||||
| ComponentWithAs<'input', InputProps>
|
||||
@ -28,7 +26,6 @@ export type TextBoxWithVariableButtonProps = {
|
||||
export const TextBoxWithVariableButton = ({
|
||||
initialValue,
|
||||
onChange,
|
||||
delay,
|
||||
TextBox,
|
||||
...props
|
||||
}: TextBoxWithVariableButtonProps) => {
|
||||
@ -36,13 +33,12 @@ export const TextBoxWithVariableButton = ({
|
||||
null
|
||||
)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, delay ?? 100)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue !== initialValue) onChange(debouncedValue)
|
||||
if (value !== initialValue) onChange(value)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
}, [value])
|
||||
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
if (!textBoxRef.current || !variable) return
|
||||
|
@ -118,6 +118,7 @@ export const VariableSearchInput = ({
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
placeholder={inputProps.placeholder ?? 'Select a variable'}
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
|
@ -42,7 +42,7 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
setColor(e.target.value)
|
||||
|
||||
return (
|
||||
<Popover variant="picker">
|
||||
<Popover variant="picker" placement="right" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
aria-label={'Pick a color'}
|
||||
|
@ -65,6 +65,8 @@ const graphContext = createContext<{
|
||||
addSourceEndpoint: (endpoint: Endpoint) => void
|
||||
targetEndpoints: Table<Endpoint>
|
||||
addTargetEndpoint: (endpoint: Endpoint) => void
|
||||
openedStepId?: string
|
||||
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({
|
||||
@ -84,6 +86,7 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
byId: {},
|
||||
allIds: [],
|
||||
})
|
||||
const [openedStepId, setOpenedStepId] = useState<string>()
|
||||
|
||||
const addSourceEndpoint = (endpoint: Endpoint) => {
|
||||
setSourceEndpoints((endpoints) => ({
|
||||
@ -112,6 +115,8 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
targetEndpoints,
|
||||
addSourceEndpoint,
|
||||
addTargetEndpoint,
|
||||
openedStepId,
|
||||
setOpenedStepId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
const dndContext = createContext<{
|
||||
const stepDndContext = createContext<{
|
||||
draggedStepType?: DraggableStepType
|
||||
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
||||
draggedStep?: DraggableStep
|
||||
@ -21,7 +21,7 @@ const dndContext = createContext<{
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const DndContext = ({ children }: { children: ReactNode }) => {
|
||||
export const StepDndContext = ({ children }: { children: ReactNode }) => {
|
||||
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
|
||||
const [draggedStepType, setDraggedStepType] = useState<
|
||||
DraggableStepType | undefined
|
||||
@ -32,7 +32,7 @@ export const DndContext = ({ children }: { children: ReactNode }) => {
|
||||
const [mouseOverBlockId, setMouseOverBlockId] = useState<string>()
|
||||
|
||||
return (
|
||||
<dndContext.Provider
|
||||
<stepDndContext.Provider
|
||||
value={{
|
||||
draggedStep,
|
||||
setDraggedStep,
|
||||
@ -45,8 +45,8 @@ export const DndContext = ({ children }: { children: ReactNode }) => {
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</dndContext.Provider>
|
||||
</stepDndContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDnd = () => useContext(dndContext)
|
||||
export const useStepDnd = () => useContext(stepDndContext)
|
45
apps/builder/contexts/TypebotDndContext.tsx
Normal file
45
apps/builder/contexts/TypebotDndContext.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Typebot } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
const typebotDndContext = createContext<{
|
||||
draggedTypebot?: Typebot
|
||||
setDraggedTypebot: Dispatch<SetStateAction<Typebot | undefined>>
|
||||
mouseOverFolderId?: string | null
|
||||
setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const TypebotDndContext = ({ children }: { children: ReactNode }) => {
|
||||
const [draggedTypebot, setDraggedTypebot] = useState<Typebot>()
|
||||
const [mouseOverFolderId, setMouseOverFolderId] = useState<string | null>()
|
||||
|
||||
useEffect(() => {
|
||||
draggedTypebot
|
||||
? document.body.classList.add('grabbing')
|
||||
: document.body.classList.remove('grabbing')
|
||||
}, [draggedTypebot])
|
||||
|
||||
return (
|
||||
<typebotDndContext.Provider
|
||||
value={{
|
||||
draggedTypebot,
|
||||
setDraggedTypebot,
|
||||
mouseOverFolderId,
|
||||
setMouseOverFolderId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</typebotDndContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTypebotDnd = () => useContext(typebotDndContext)
|
@ -82,11 +82,6 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
const refreshUser = async () => {
|
||||
await fetch('/api/auth/session?update')
|
||||
reloadSession()
|
||||
}
|
||||
|
||||
return (
|
||||
<userContext.Provider
|
||||
value={{
|
||||
@ -105,6 +100,11 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const refreshUser = async () => {
|
||||
await fetch('/api/auth/session?update')
|
||||
reloadSession()
|
||||
}
|
||||
|
||||
const reloadSession = () => {
|
||||
const event = new Event('visibilitychange')
|
||||
document.dispatchEvent(event)
|
||||
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"chromeWebSecurity": false,
|
||||
"integrationFolder": "cypress/tests",
|
||||
"viewportWidth": 1400,
|
||||
"viewportHeight": 800,
|
||||
"video": false
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import { Step } from 'models'
|
||||
import { parseTestTypebot } from './utils'
|
||||
|
||||
export const users = [
|
||||
{ id: 'user1', email: 'test1@gmail.com' },
|
||||
{ id: 'user2', email: 'test2@gmail.com' },
|
||||
]
|
||||
|
||||
export const createTypebotWithStep = (step: Omit<Step, 'id' | 'blockId'>) => {
|
||||
cy.task(
|
||||
'createTypebot',
|
||||
parseTestTypebot({
|
||||
id: 'typebot3',
|
||||
name: 'Typebot #3',
|
||||
ownerId: users[1].id,
|
||||
steps: {
|
||||
byId: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
step1: {
|
||||
...step,
|
||||
id: 'step1',
|
||||
blockId: 'block1',
|
||||
},
|
||||
},
|
||||
allIds: ['step1'],
|
||||
},
|
||||
blocks: {
|
||||
byId: {
|
||||
block1: {
|
||||
id: 'block1',
|
||||
graphCoordinates: { x: 400, y: 200 },
|
||||
title: 'Block #1',
|
||||
stepIds: ['step1'],
|
||||
},
|
||||
},
|
||||
allIds: ['block1'],
|
||||
},
|
||||
choiceItems: {
|
||||
byId: { item1: { stepId: 'step1', id: 'item1' } },
|
||||
allIds: ['item1'],
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
import {
|
||||
defaultTextInputOptions,
|
||||
InputStepType,
|
||||
PublicTypebot,
|
||||
TextInputStep,
|
||||
Typebot,
|
||||
} from 'models'
|
||||
import { CredentialsType, Plan, PrismaClient } from 'db'
|
||||
import { parseTestTypebot } from './utils'
|
||||
import { users } from './data'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const teardownTestData = async () => prisma.user.deleteMany()
|
||||
|
||||
export const seedDb = async (googleRefreshToken: string) => {
|
||||
await teardownTestData()
|
||||
await createUsers()
|
||||
await createCredentials(googleRefreshToken)
|
||||
await createFolders()
|
||||
await createTypebots()
|
||||
await createResults()
|
||||
return createAnswers()
|
||||
}
|
||||
|
||||
export const createTypebot = (typebot: Typebot) =>
|
||||
prisma.typebot.create({ data: typebot as any })
|
||||
|
||||
const createUsers = () =>
|
||||
prisma.user.createMany({
|
||||
data: [
|
||||
{ ...users[0], emailVerified: new Date() },
|
||||
{
|
||||
...users[1],
|
||||
emailVerified: new Date(),
|
||||
plan: Plan.PRO,
|
||||
stripeId: 'stripe-test2',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const createCredentials = (refresh_token: string) =>
|
||||
prisma.credentials.createMany({
|
||||
data: [
|
||||
{
|
||||
name: 'test2@gmail.com',
|
||||
ownerId: users[1].id,
|
||||
type: CredentialsType.GOOGLE_SHEETS,
|
||||
data: {
|
||||
expiry_date: 1642441058842,
|
||||
access_token:
|
||||
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
|
||||
refresh_token,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const createFolders = () =>
|
||||
prisma.dashboardFolder.createMany({
|
||||
data: [{ ownerId: users[1].id, name: 'Folder #1', id: 'folder1' }],
|
||||
})
|
||||
|
||||
const createTypebots = async () => {
|
||||
const typebot2 = {
|
||||
...parseTestTypebot({
|
||||
id: 'typebot2',
|
||||
name: 'Typebot #2',
|
||||
ownerId: users[1].id,
|
||||
blocks: {
|
||||
byId: {
|
||||
block1: {
|
||||
id: 'block1',
|
||||
title: 'Block #1',
|
||||
stepIds: ['step1'],
|
||||
graphCoordinates: { x: 200, y: 200 },
|
||||
},
|
||||
},
|
||||
allIds: ['block1'],
|
||||
},
|
||||
steps: {
|
||||
byId: {
|
||||
step1: {
|
||||
id: 'step1',
|
||||
type: InputStepType.TEXT,
|
||||
blockId: 'block1',
|
||||
options: defaultTextInputOptions,
|
||||
} as TextInputStep,
|
||||
},
|
||||
allIds: ['step1'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
await prisma.typebot.createMany({
|
||||
data: [
|
||||
{
|
||||
...parseTestTypebot({
|
||||
id: 'typebot1',
|
||||
name: 'Typebot #1',
|
||||
ownerId: users[1].id,
|
||||
blocks: { byId: {}, allIds: [] },
|
||||
steps: { byId: {}, allIds: [] },
|
||||
}),
|
||||
},
|
||||
typebot2,
|
||||
] as any,
|
||||
})
|
||||
return prisma.publicTypebot.createMany({
|
||||
data: [parseTypebotToPublicTypebot('publictypebot2', typebot2)] as any,
|
||||
})
|
||||
}
|
||||
|
||||
const createResults = () => {
|
||||
return prisma.result.createMany({
|
||||
data: [
|
||||
...Array.from(Array(200)).map((_, idx) => {
|
||||
const today = new Date()
|
||||
return {
|
||||
id: `result${idx}`,
|
||||
typebotId: 'typebot2',
|
||||
createdAt: new Date(
|
||||
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
|
||||
),
|
||||
isCompleted: false,
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const createAnswers = () => {
|
||||
return prisma.answer.createMany({
|
||||
data: [
|
||||
...Array.from(Array(200)).map((_, idx) => ({
|
||||
resultId: `result${idx}`,
|
||||
content: `content${idx}`,
|
||||
stepId: 'step1',
|
||||
blockId: 'block1',
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const parseTypebotToPublicTypebot = (
|
||||
id: string,
|
||||
typebot: Typebot
|
||||
): PublicTypebot => ({
|
||||
id,
|
||||
blocks: typebot.blocks,
|
||||
steps: typebot.steps,
|
||||
name: typebot.name,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
publicId: typebot.publicId,
|
||||
choiceItems: typebot.choiceItems,
|
||||
variables: typebot.variables,
|
||||
edges: typebot.edges,
|
||||
})
|
||||
|
||||
export const loadRawTypebotInDatabase = (typebot: Typebot) =>
|
||||
prisma.typebot.create({
|
||||
data: { ...typebot, id: 'typebot4', ownerId: users[1].id } as any,
|
||||
})
|
@ -1,24 +0,0 @@
|
||||
import {
|
||||
GitHubSocialLogin,
|
||||
FacebookSocialLogin,
|
||||
GoogleSocialLogin,
|
||||
} from 'cypress-social-logins/src/Plugins'
|
||||
import { createTypebot, loadRawTypebotInDatabase, seedDb } from './database'
|
||||
/// <reference types="cypress" />
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
|
||||
const handler = (on: any) => {
|
||||
on('task', {
|
||||
GoogleSocialLogin: GoogleSocialLogin,
|
||||
FacebookSocialLogin: FacebookSocialLogin,
|
||||
GitHubSocialLogin: GitHubSocialLogin,
|
||||
seed: seedDb,
|
||||
createTypebot,
|
||||
loadRawTypebotInDatabase,
|
||||
})
|
||||
}
|
||||
|
||||
export default handler
|
@ -1,81 +0,0 @@
|
||||
import {
|
||||
Block,
|
||||
Typebot,
|
||||
Table,
|
||||
Step,
|
||||
ChoiceItem,
|
||||
defaultTheme,
|
||||
defaultSettings,
|
||||
} from 'models'
|
||||
|
||||
export const parseTestTypebot = ({
|
||||
id,
|
||||
ownerId,
|
||||
name,
|
||||
blocks,
|
||||
steps,
|
||||
choiceItems,
|
||||
}: {
|
||||
id: string
|
||||
ownerId: string
|
||||
name: string
|
||||
blocks: Table<Block>
|
||||
steps: Table<Step>
|
||||
choiceItems?: Table<ChoiceItem>
|
||||
}): Typebot => {
|
||||
return {
|
||||
id,
|
||||
folderId: null,
|
||||
name,
|
||||
ownerId,
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
createdAt: new Date(),
|
||||
blocks: {
|
||||
byId: {
|
||||
block0: {
|
||||
id: 'block0',
|
||||
title: 'Block #0',
|
||||
stepIds: ['step0'],
|
||||
graphCoordinates: { x: 0, y: 0 },
|
||||
},
|
||||
...blocks.byId,
|
||||
},
|
||||
allIds: ['block0', ...blocks.allIds],
|
||||
},
|
||||
steps: {
|
||||
byId: {
|
||||
step0: {
|
||||
id: 'step0',
|
||||
type: 'start',
|
||||
blockId: 'block0',
|
||||
label: 'Start',
|
||||
edgeId: 'edge1',
|
||||
},
|
||||
...steps.byId,
|
||||
},
|
||||
allIds: ['step0', ...steps.allIds],
|
||||
},
|
||||
choiceItems: choiceItems ?? { byId: {}, allIds: [] },
|
||||
publicId: null,
|
||||
publishedTypebotId: null,
|
||||
updatedAt: new Date(),
|
||||
variables: { byId: {}, allIds: [] },
|
||||
webhooks: { byId: {}, allIds: [] },
|
||||
edges: {
|
||||
byId: {
|
||||
edge1: {
|
||||
id: 'edge1',
|
||||
from: { blockId: 'block0', stepId: 'step0' },
|
||||
to: { blockId: 'block1' },
|
||||
},
|
||||
},
|
||||
allIds: ['edge1'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const preventUserFromRefreshing = (e: BeforeUnloadEvent) => {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
import { signIn, signOut } from 'next-auth/react'
|
||||
|
||||
Cypress.Commands.add('signOut', () => {
|
||||
cy.log(`🔐 Sign out`)
|
||||
return cy.wrap(signOut({ redirect: false }), { log: false })
|
||||
})
|
||||
|
||||
Cypress.Commands.add('signIn', (email: string) => {
|
||||
cy.log(`🔐 Sign in as ${email}`)
|
||||
return cy.wrap(signIn('credentials', { redirect: false, email }), {
|
||||
log: false,
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('loadTypebotFixtureInDatabase', (path: string) => {
|
||||
return cy.fixture(path).then((typebot) => {
|
||||
cy.task('loadRawTypebotInDatabase', typebot)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add(
|
||||
'mouseMoveBy',
|
||||
{
|
||||
prevSubject: 'element',
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(
|
||||
subject: JQuery<HTMLElement>,
|
||||
x: number,
|
||||
y: number,
|
||||
options?: { delay: number }
|
||||
) => {
|
||||
cy.wrap(subject, { log: false })
|
||||
.then((subject) => {
|
||||
const initialRect = subject.get(0).getBoundingClientRect()
|
||||
const windowScroll = getDocumentScroll()
|
||||
|
||||
return [subject, initialRect, windowScroll] as const
|
||||
})
|
||||
.then(([subject, initialRect, initialWindowScroll]) => {
|
||||
cy.wrap(subject)
|
||||
.trigger('mousedown', { force: true })
|
||||
.wait(options?.delay || 0, { log: Boolean(options?.delay) })
|
||||
.trigger('mousemove', {
|
||||
force: true,
|
||||
clientX: Math.floor(
|
||||
initialRect.left + initialRect.width / 2 + x / 2
|
||||
),
|
||||
clientY: Math.floor(
|
||||
initialRect.top + initialRect.height / 2 + y / 2
|
||||
),
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
force: true,
|
||||
clientX: Math.floor(initialRect.left + initialRect.width / 2 + x),
|
||||
clientY: Math.floor(initialRect.top + initialRect.height / 2 + y),
|
||||
})
|
||||
// .wait(1000)
|
||||
.trigger('mouseup', { force: true })
|
||||
.wait(250)
|
||||
.then((subject: any) => {
|
||||
const finalRect = subject.get(0).getBoundingClientRect()
|
||||
const windowScroll = getDocumentScroll()
|
||||
const windowScrollDelta = {
|
||||
x: windowScroll.x - initialWindowScroll.x,
|
||||
y: windowScroll.y - initialWindowScroll.y,
|
||||
}
|
||||
|
||||
const delta = {
|
||||
x: Math.round(
|
||||
finalRect.left - initialRect.left - windowScrollDelta.x
|
||||
),
|
||||
y: Math.round(
|
||||
finalRect.top - initialRect.top - windowScrollDelta.y
|
||||
),
|
||||
}
|
||||
|
||||
return [subject, { initialRect, finalRect, delta }] as const
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add('createVariable', (name: string) => {
|
||||
cy.findByTestId('variables-input').type(name)
|
||||
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
|
||||
})
|
||||
|
||||
const getDocumentScroll = () => {
|
||||
if (document.scrollingElement) {
|
||||
const { scrollTop, scrollLeft } = document.scrollingElement
|
||||
|
||||
return {
|
||||
x: scrollTop,
|
||||
y: scrollLeft,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
signOut(): Chainable<any>
|
||||
signIn(email: string): Chainable<any>
|
||||
loadTypebotFixtureInDatabase(path: string): Chainable<any>
|
||||
createVariable(name: string): Chainable<any>
|
||||
mouseMoveBy(
|
||||
x: number,
|
||||
y: number,
|
||||
options?: { delay: number }
|
||||
): Chainable<
|
||||
[
|
||||
Element,
|
||||
{
|
||||
initialRect: ClientRect
|
||||
finalRect: ClientRect
|
||||
delta: { x: number; y: number }
|
||||
}
|
||||
]
|
||||
>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
|
||||
Cypress.on('uncaught:exception', (err) => {
|
||||
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
export const prepareDbAndSignIn = () => {
|
||||
cy.signOut()
|
||||
cy.task('seed')
|
||||
cy.signIn(users[1].email)
|
||||
}
|
||||
|
||||
export const removePreventReload = () => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
}
|
||||
|
||||
export const getIframeBody = () => {
|
||||
return cy
|
||||
.get('#typebot-iframe')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
.then(cy.wrap)
|
||||
}
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import '@testing-library/cypress/add-commands'
|
||||
import 'cypress-file-upload'
|
||||
import { users } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
@ -1,65 +0,0 @@
|
||||
import { users } from 'cypress/plugins/data'
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
|
||||
describe('Account page', () => {
|
||||
before(() => {
|
||||
cy.intercept({
|
||||
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
|
||||
method: 'POST',
|
||||
}).as('postImage')
|
||||
cy.intercept({ url: '/api/auth/session?update', method: 'GET' }).as(
|
||||
'getUpdatedSession'
|
||||
)
|
||||
})
|
||||
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should edit user info properly', () => {
|
||||
cy.signIn(users[0].email)
|
||||
cy.visit('/account')
|
||||
cy.findByRole('button', { name: 'Save' }).should('not.exist')
|
||||
cy.findByRole('textbox', { name: 'Email address' }).should(
|
||||
'have.attr',
|
||||
'disabled'
|
||||
)
|
||||
cy.findByRole('textbox', { name: 'Name' })
|
||||
.should('have.value', '')
|
||||
.type('John Doe')
|
||||
|
||||
cy.findByRole('img').should('not.have.attr', 'src')
|
||||
cy.findByLabelText('Change photo').attachFile('avatar.jpg')
|
||||
cy.wait('@postImage')
|
||||
cy.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should(
|
||||
'include',
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/users/${users[0].id}/avatar`
|
||||
)
|
||||
cy.findByRole('button', { name: 'Save' }).should('exist').click()
|
||||
cy.wait('@getUpdatedSession')
|
||||
.then((interception) => {
|
||||
return interception.response?.statusCode
|
||||
})
|
||||
.should('eq', 200)
|
||||
cy.findByRole('button', { name: 'Save' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('should display valid plans', () => {
|
||||
cy.signIn(users[0].email)
|
||||
cy.visit('/account')
|
||||
cy.findByText('Free plan').should('exist')
|
||||
cy.findByRole('link', { name: 'Manage my subscription' }).should(
|
||||
'not.exist'
|
||||
)
|
||||
cy.findByRole('button', { name: 'Upgrade' }).should('exist')
|
||||
cy.signOut()
|
||||
cy.signIn(users[1].email)
|
||||
cy.visit('/account')
|
||||
cy.findByText('Pro plan').should('exist')
|
||||
cy.findByRole('link', { name: 'Manage my subscription' })
|
||||
.should('have.attr', 'href')
|
||||
.should('include', 'customer-portal')
|
||||
})
|
||||
})
|
@ -1,81 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { BubbleStepType, defaultImageBubbleContent, Step } from 'models'
|
||||
|
||||
const unsplashImageSrc =
|
||||
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
||||
|
||||
describe('Image bubbles', () => {
|
||||
before(() => {
|
||||
cy.intercept({
|
||||
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
|
||||
method: 'POST',
|
||||
}).as('postImage')
|
||||
})
|
||||
|
||||
describe('Content settings', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.IMAGE,
|
||||
content: defaultImageBubbleContent,
|
||||
} as Step)
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByText('Click to edit...').click()
|
||||
})
|
||||
|
||||
it('upload image file correctly', () => {
|
||||
cy.findByTestId('file-upload-input').attachFile('avatar.jpg')
|
||||
cy.wait('@postImage')
|
||||
cy.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should(
|
||||
'include',
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/typebots/typebot3/avatar.jpg`
|
||||
)
|
||||
})
|
||||
|
||||
it('should import image links correctly', () => {
|
||||
cy.findByRole('button', { name: 'Embed link' }).click()
|
||||
cy.findByPlaceholderText('Paste the image link...')
|
||||
.clear()
|
||||
.type(unsplashImageSrc)
|
||||
cy.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should('include', unsplashImageSrc)
|
||||
})
|
||||
|
||||
it('should import giphy gifs correctly', () => {
|
||||
cy.findByRole('button', { name: 'Giphy' }).click()
|
||||
cy.findAllByRole('img').eq(3).click()
|
||||
cy.findAllByRole('img')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('contain', `giphy.com/media`)
|
||||
})
|
||||
})
|
||||
describe('Preview', () => {
|
||||
before(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.IMAGE,
|
||||
content: {
|
||||
url: unsplashImageSrc,
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
})
|
||||
|
||||
it('should display correctly', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', unsplashImageSrc)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,48 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { BubbleStepType, defaultTextBubbleContent, Step } from 'models'
|
||||
|
||||
describe('Text bubbles', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.TEXT,
|
||||
content: defaultTextBubbleContent,
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('rich text features should work', () => {
|
||||
cy.findByTestId('bold-button').click()
|
||||
cy.findByRole('textbox', { name: 'Text editor' }).type('Bold text{enter}')
|
||||
cy.findByTestId('bold-button').click()
|
||||
cy.findByTestId('italic-button').click()
|
||||
cy.findByRole('textbox', { name: 'Text editor' }).type('Italic text{enter}')
|
||||
cy.findByTestId('italic-button').click()
|
||||
cy.findByTestId('underline-button').click()
|
||||
cy.findByRole('textbox', { name: 'Text editor' }).type(
|
||||
'Underlined text{enter}'
|
||||
)
|
||||
cy.findByTestId('bold-button').click()
|
||||
cy.findByTestId('italic-button').click()
|
||||
cy.findByRole('textbox', { name: 'Text editor' }).type('Everything text')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('span.slate-bold')
|
||||
.should('exist')
|
||||
.should('contain.text', 'Bold text')
|
||||
getIframeBody()
|
||||
.get('span.slate-italic')
|
||||
.should('exist')
|
||||
.should('contain.text', 'Italic text')
|
||||
getIframeBody()
|
||||
.get('span.slate-underline')
|
||||
.should('exist')
|
||||
.should('contain.text', 'Underlined text')
|
||||
})
|
||||
})
|
@ -1,114 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import {
|
||||
BubbleStepType,
|
||||
defaultVideoBubbleContent,
|
||||
Step,
|
||||
VideoBubbleContentType,
|
||||
} from 'models'
|
||||
|
||||
const videoSrc =
|
||||
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
|
||||
const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
const vimeoVideoSrc = 'https://vimeo.com/649301125'
|
||||
|
||||
describe('Video bubbles', () => {
|
||||
describe('Content settings', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: defaultVideoBubbleContent,
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('upload image file correctly', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByText('Click to edit...').click()
|
||||
cy.findByPlaceholderText('Paste the video link...').type(videoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('video > source').should('have.attr', 'src').should('eq', videoSrc)
|
||||
|
||||
cy.findByPlaceholderText('Paste the video link...')
|
||||
.clear()
|
||||
.type(youtubeVideoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('iframe')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||
|
||||
cy.findByPlaceholderText('Paste the video link...')
|
||||
.clear()
|
||||
.type(vimeoVideoSrc, {
|
||||
waitForAnimations: false,
|
||||
})
|
||||
cy.get('iframe')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preview', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should display video correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.URL,
|
||||
url: videoSrc,
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('video > source')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', videoSrc)
|
||||
})
|
||||
|
||||
it('should display youtube iframe correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.YOUTUBE,
|
||||
url: youtubeVideoSrc,
|
||||
id: 'dQw4w9WgXcQ',
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('iframe')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||
})
|
||||
|
||||
it('should display vimeo iframe correctly', () => {
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.VIDEO,
|
||||
content: {
|
||||
type: VideoBubbleContentType.VIMEO,
|
||||
url: vimeoVideoSrc,
|
||||
id: '649301125',
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.get('iframe')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||
})
|
||||
})
|
||||
})
|
@ -1,64 +0,0 @@
|
||||
import { users } from 'cypress/plugins/data'
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
|
||||
describe('Dashboard page', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('folders navigation should work', () => {
|
||||
cy.signIn(users[0].email)
|
||||
cy.visit('/typebots')
|
||||
createFolder('My folder #1')
|
||||
cy.findByTestId('folder-button').click()
|
||||
cy.findByRole('heading', { name: 'My folder #1' }).should('exist')
|
||||
createFolder('My folder #2')
|
||||
cy.findByTestId('folder-button').click()
|
||||
cy.findByRole('heading', { name: 'My folder #2' }).should('exist')
|
||||
cy.findByRole('link', { name: 'Back' }).click()
|
||||
cy.findByRole('heading', { name: 'My folder #1' }).should('exist')
|
||||
cy.findByRole('link', { name: 'Back' }).click()
|
||||
cy.findByRole('button', { name: 'Show folder menu' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findByDisplayValue('My folder #2').should('exist')
|
||||
cy.findByRole('button', { name: 'Show folder menu' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findByDisplayValue('My folder #2').should('not.exist')
|
||||
})
|
||||
|
||||
it('folders and typebots should be deletable', () => {
|
||||
cy.visit('/typebots')
|
||||
cy.findByText('Folder #1').should('exist')
|
||||
cy.findAllByRole('button', { name: 'Show folder menu' }).first().click()
|
||||
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findByText('Folder #1').should('not.exist')
|
||||
cy.findByText('Typebot #1').should('exist')
|
||||
cy.findAllByRole('button', { name: 'Show typebot menu' }).first().click()
|
||||
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findByText('Typebot #1').should('not.exist')
|
||||
})
|
||||
|
||||
it('folders should be draggable and droppable', () => {
|
||||
cy.visit('/typebots')
|
||||
cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-100, 0, {
|
||||
delay: 120,
|
||||
})
|
||||
cy.visit('/typebots/folders/folder1')
|
||||
cy.findByTestId('typebot-button-typebot1').mouseMoveBy(-300, -100, {
|
||||
delay: 120,
|
||||
})
|
||||
cy.visit('/typebots')
|
||||
cy.findByDisplayValue('Folder #1').should('exist')
|
||||
cy.findByText('Typebot #1').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
const createFolder = (folderName: string) => {
|
||||
cy.findByRole('button', { name: 'Create a folder' }).click({ force: true })
|
||||
cy.findByText('New folder').click({ force: true })
|
||||
cy.findByDisplayValue('New folder').type(`${folderName}{enter}`)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultChoiceInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Button input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.CHOICE,
|
||||
options: { ...defaultChoiceInputOptions, itemIds: ['item1'] },
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('Can edit choice items', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
|
||||
cy.findByText('Item 1').trigger('mouseover')
|
||||
cy.findByRole('button', { name: 'Add item' }).click()
|
||||
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
|
||||
cy.findByRole('button', { name: 'Add item' }).click()
|
||||
cy.findByDisplayValue('Click to edit').type('Item 3{enter}')
|
||||
cy.findByText('Item 2').rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||
cy.findByText('Item 2').should('not.exist')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Item 3' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Item 3' }).should('not.exist')
|
||||
getIframeBody().findByText('Item 3')
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('checkbox', { name: 'Multiple choice?' }).check({
|
||||
force: true,
|
||||
})
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.wait(200)
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByText('Item 1').trigger('mouseover')
|
||||
cy.findByRole('button', { name: 'Add item' }).click()
|
||||
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('checkbox', { name: 'Item 3' }).click()
|
||||
getIframeBody().findByRole('checkbox', { name: 'Item 1' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody().findByText('Item 3, Item 1').should('exist')
|
||||
})
|
||||
|
||||
it('Single choice targets should work', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/singleChoiceTarget.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Burgers' }).click()
|
||||
getIframeBody().findByText('I love burgers!').should('exist')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Carpaccio' }).click()
|
||||
getIframeBody().findByText('Cool!').should('exist')
|
||||
})
|
||||
})
|
@ -1,44 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultDateInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Date input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.DATE,
|
||||
options: defaultDateInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByTestId('from-date')
|
||||
.should('have.attr', 'type')
|
||||
.should('eq', 'date')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('checkbox', { name: 'Is range?' }).check({ force: true })
|
||||
cy.findByRole('textbox', { name: 'From label:' }).clear().type('Previous:')
|
||||
cy.findByRole('textbox', { name: 'To label:' }).clear().type('After:')
|
||||
cy.findByRole('checkbox', { name: 'With time?' }).check({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody()
|
||||
.findByTestId('from-date')
|
||||
.should('have.attr', 'type')
|
||||
.should('eq', 'datetime-local')
|
||||
getIframeBody()
|
||||
.findByTestId('to-date')
|
||||
.should('have.attr', 'type')
|
||||
.should('eq', 'datetime-local')
|
||||
getIframeBody().findByRole('button', { name: 'Go' })
|
||||
})
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultEmailInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Email input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.EMAIL,
|
||||
options: defaultEmailInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your email...')
|
||||
.should('have.attr', 'type')
|
||||
.should('equal', 'email')
|
||||
getIframeBody().findByRole('button', { name: 'Send' })
|
||||
getIframeBody()
|
||||
.findByPlaceholderText(defaultEmailInputOptions.labels.placeholder)
|
||||
.should('exist')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Placeholder:' })
|
||||
.clear()
|
||||
.type('Your email...')
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByTestId('step-step1').should('contain.text', 'Your email...')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody().findByPlaceholderText('Your email...').should('exist')
|
||||
getIframeBody().findByRole('button', { name: 'Go' })
|
||||
})
|
||||
})
|
@ -1,46 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultNumberInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Number input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.NUMBER,
|
||||
options: defaultNumberInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText(defaultNumberInputOptions.labels.placeholder)
|
||||
.should('have.attr', 'type')
|
||||
.should('equal', 'number')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Placeholder:' })
|
||||
.clear()
|
||||
.type('Your name...')
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByRole('spinbutton', { name: 'Min:' }).type('0')
|
||||
cy.findByRole('spinbutton', { name: 'Max:' }).type('100')
|
||||
cy.findByRole('spinbutton', { name: 'Step:' }).type('10')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Your name...')
|
||||
.should('exist')
|
||||
.type('-1{enter}')
|
||||
.clear()
|
||||
.type('150{enter}')
|
||||
getIframeBody().findByRole('button', { name: 'Go' })
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
})
|
||||
})
|
@ -1,43 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultPhoneInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Phone number input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.PHONE,
|
||||
options: defaultPhoneInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText(defaultPhoneInputOptions.labels.placeholder)
|
||||
.should('have.attr', 'type')
|
||||
.should('eq', 'tel')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Placeholder:' })
|
||||
.clear()
|
||||
.type('+33 XX XX XX XX')
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('+33 XX XX XX XX')
|
||||
.type('+33 6 73 18 45 36')
|
||||
getIframeBody()
|
||||
.findByRole('img')
|
||||
.should('have.attr', 'alt')
|
||||
.should('eq', 'France')
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody().findByText('+33673184536').should('exist')
|
||||
})
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultTextInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('Text input', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.TEXT,
|
||||
options: defaultTextInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText(defaultTextInputOptions.labels.placeholder)
|
||||
.should('have.attr', 'type')
|
||||
.should('equal', 'text')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Placeholder:' })
|
||||
.clear()
|
||||
.type('Your name...')
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
cy.findByTestId('step-step1').should('contain.text', 'Your name...')
|
||||
getIframeBody().findByPlaceholderText('Your name...').should('exist')
|
||||
getIframeBody().findByRole('button', { name: 'Go' })
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('checkbox', { name: 'Long text?' }).check({ force: true })
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody().findByTestId('textarea').should('exist')
|
||||
})
|
||||
})
|
@ -1,38 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
import { defaultUrlInputOptions, InputStepType, Step } from 'models'
|
||||
|
||||
describe('URL input', () => {
|
||||
afterEach(removePreventReload)
|
||||
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: InputStepType.URL,
|
||||
options: defaultUrlInputOptions,
|
||||
} as Step)
|
||||
})
|
||||
|
||||
it('options should work', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText(defaultUrlInputOptions.labels.placeholder)
|
||||
.should('have.attr', 'type')
|
||||
.should('eq', 'url')
|
||||
getIframeBody().findByRole('button', { name: 'Send' }).should('be.disabled')
|
||||
cy.findByTestId('step-step1').click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Placeholder:' })
|
||||
.clear()
|
||||
.type('Your URL...')
|
||||
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||
cy.findByTestId('step-step1').should('contain.text', 'Your URL...')
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody().findByPlaceholderText('Your URL...').should('exist')
|
||||
getIframeBody().findByRole('button', { name: 'Go' })
|
||||
})
|
||||
})
|
@ -1,37 +0,0 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
import {
|
||||
defaultGoogleAnalyticsOptions,
|
||||
IntegrationStepType,
|
||||
Step,
|
||||
} from 'models'
|
||||
|
||||
describe('Google Analytics', () => {
|
||||
afterEach(removePreventReload)
|
||||
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
createTypebotWithStep({
|
||||
type: IntegrationStepType.GOOGLE_ANALYTICS,
|
||||
options: defaultGoogleAnalyticsOptions,
|
||||
} as Step)
|
||||
})
|
||||
|
||||
it('can be filled correctly', () => {
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.intercept({
|
||||
url: '/g/collect',
|
||||
method: 'POST',
|
||||
}).as('gaRequest')
|
||||
cy.findByTestId('step-step1').click()
|
||||
cy.findByRole('textbox', { name: 'Tracking ID:' }).type('G-VWX9WG1TNS')
|
||||
cy.findByRole('textbox', { name: 'Event category:' }).type('Typebot')
|
||||
cy.findByRole('textbox', { name: 'Event action:' }).type('Submit email')
|
||||
cy.findByRole('button', { name: 'Advanced' }).click()
|
||||
cy.findByRole('textbox', { name: 'Event label Optional :' }).type(
|
||||
'Campaign Z'
|
||||
)
|
||||
cy.findByRole('textbox', { name: 'Event value Optional :' }).type('20')
|
||||
// Not sure how to test if GA integration works correctly in the preview tab
|
||||
})
|
||||
})
|
@ -1,133 +0,0 @@
|
||||
import { users } from 'cypress/plugins/data'
|
||||
import { getIframeBody, removePreventReload } from 'cypress/support'
|
||||
|
||||
describe('Google sheets', () => {
|
||||
afterEach(removePreventReload)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.signOut()
|
||||
cy.task('seed', Cypress.env('GOOGLE_SHEETS_REFRESH_TOKEN'))
|
||||
cy.signIn(users[1].email)
|
||||
})
|
||||
|
||||
it('Insert row should work', () => {
|
||||
cy.intercept({
|
||||
url: '/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0',
|
||||
method: 'POST',
|
||||
}).as('insertRowInGoogleSheets')
|
||||
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
|
||||
fillInSpreadsheetInfo()
|
||||
|
||||
cy.findByRole('button', { name: 'Select an operation' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Insert a row' }).click({ force: true })
|
||||
|
||||
cy.findByRole('button', { name: 'Select a column' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
cy.findByRole('button', { name: 'Insert a variable' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Add a value' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Select a column' }).click()
|
||||
cy.findByRole('menuitem', { name: 'First name' }).click()
|
||||
cy.findAllByPlaceholderText('Type a value...').last().type('Georges')
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your email...')
|
||||
.type('georges@gmail.com{enter}')
|
||||
cy.wait('@insertRowInGoogleSheets')
|
||||
.then((interception) => {
|
||||
return interception.response?.statusCode
|
||||
})
|
||||
.should('eq', 200)
|
||||
})
|
||||
|
||||
it('Update row should work', () => {
|
||||
cy.intercept({
|
||||
url: '/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0',
|
||||
method: 'PATCH',
|
||||
}).as('updateRowInGoogleSheets')
|
||||
cy.loadTypebotFixtureInDatabase('typebots/integrations/googleSheets.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
|
||||
fillInSpreadsheetInfo()
|
||||
|
||||
cy.findByRole('button', { name: 'Select an operation' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Update a row' }).click({ force: true })
|
||||
|
||||
cy.findAllByRole('button', { name: 'Select a column' }).first().click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
cy.findAllByRole('button', { name: 'Insert a variable' }).first().click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Select a column' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Last name' }).click()
|
||||
cy.findAllByPlaceholderText('Type a value...').last().type('Last name')
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your email...')
|
||||
.type('test@test.com{enter}')
|
||||
cy.wait('@updateRowInGoogleSheets')
|
||||
.then((interception) => {
|
||||
return interception.response?.statusCode
|
||||
})
|
||||
.should('eq', 200)
|
||||
})
|
||||
|
||||
it('Get row should work', () => {
|
||||
cy.loadTypebotFixtureInDatabase(
|
||||
'typebots/integrations/googleSheetsGet.json'
|
||||
)
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
|
||||
fillInSpreadsheetInfo()
|
||||
|
||||
cy.findByRole('button', { name: 'Select an operation' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Get data from sheet' }).click({
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.findAllByRole('button', { name: 'Select a column' }).first().click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
cy.findByRole('button', { name: 'Insert a variable' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Email' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Select a column' }).click()
|
||||
cy.findByRole('menuitem', { name: 'First name' }).click()
|
||||
createNewVar('First name')
|
||||
|
||||
cy.findByRole('button', { name: 'Add a value' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Select a column' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Last name' }).click()
|
||||
createNewVar('Last name')
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your email...')
|
||||
.type('test2@test.com{enter}')
|
||||
getIframeBody().findByText('Your name is: John Smith').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
const fillInSpreadsheetInfo = () => {
|
||||
cy.findByText('Configure...').click()
|
||||
|
||||
cy.findByRole('button', { name: 'Select an account' }).click()
|
||||
cy.findByRole('menuitem', { name: 'test2@gmail.com' }).click()
|
||||
|
||||
cy.findByPlaceholderText('Search for spreadsheet').type('CR')
|
||||
cy.findByRole('menuitem', { name: 'CRM' }).click()
|
||||
|
||||
cy.findByPlaceholderText('Select the sheet').type('Sh')
|
||||
cy.findByRole('menuitem', { name: 'Sheet1' }).click()
|
||||
}
|
||||
|
||||
const createNewVar = (name: string) => {
|
||||
cy.findAllByTestId('variables-input').last().type(name)
|
||||
cy.findByRole('menuitem', { name: `Create "${name}"` }).click()
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('Webhook step', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('configuration is working', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByText('Configure...').click()
|
||||
cy.findByRole('button', { name: 'GET' }).click()
|
||||
cy.findByRole('menuitem', { name: 'POST' }).click({ force: true })
|
||||
cy.findByPlaceholderText('Your Webhook URL...').type(
|
||||
`${Cypress.env('SITE_NAME')}/api/mock/webhook`
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Query params' }).click()
|
||||
cy.findByRole('textbox', { name: 'Key:' }).type('firstParam')
|
||||
cy.findByRole('textbox', { name: 'Value:' }).type('{{secret 1}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
cy.findByRole('button', { name: 'Add a param' }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Key:' }).last().type('secondParam')
|
||||
cy.findAllByRole('textbox', { name: 'Value:' })
|
||||
.last()
|
||||
.type('{{secret 2}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Headers' }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Key:' })
|
||||
.last()
|
||||
.type('Custom-Typebot')
|
||||
cy.findAllByRole('textbox', { name: 'Value:' })
|
||||
.last()
|
||||
.type('{{secret 3}}', {
|
||||
parseSpecialCharSequences: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Body' }).click()
|
||||
cy.findByTestId('code-editor').type('{ "customField": "{{secret 4}}" }', {
|
||||
parseSpecialCharSequences: false,
|
||||
waitForAnimations: false,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Variable values for test' }).click()
|
||||
addTestVariable('secret 1', 'secret1')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 2', 'secret2')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 3', 'secret3')
|
||||
cy.findByRole('button', { name: 'Add an entry' }).click()
|
||||
addTestVariable('secret 4', 'secret4')
|
||||
|
||||
cy.findByRole('button', { name: 'Test the request' }).click()
|
||||
|
||||
cy.findAllByTestId('code-editor')
|
||||
.should('have.length', 2)
|
||||
.last()
|
||||
.should('contain.text', '"statusCode": 200')
|
||||
|
||||
cy.findByRole('button', { name: 'Save in variables' }).click()
|
||||
cy.findByPlaceholderText('Select the data').click()
|
||||
cy.findByRole('menuitem', { name: 'data[0].name' }).click()
|
||||
})
|
||||
})
|
||||
describe('Preview', () => {
|
||||
it('should correctly send the request', () => {
|
||||
cy.loadTypebotFixtureInDatabase(
|
||||
'typebots/integrations/webhookPreview.json'
|
||||
)
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody().findByText('His name is John').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const addTestVariable = (name: string, value: string) => {
|
||||
cy.findAllByTestId('variables-input').last().click()
|
||||
cy.findByRole('menuitem', { name }).click()
|
||||
cy.findAllByRole('textbox', { name: 'Test value:' }).last().type(value)
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('Condition step', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('options should work', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/logic/condition.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
|
||||
cy.findAllByText('Equal to').first().click()
|
||||
|
||||
cy.findByTestId('variables-input').click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Greater than' }).click()
|
||||
cy.findByPlaceholderText('Type a value...').type('80')
|
||||
|
||||
cy.findByRole('button', { name: 'Add a comparison' }).click()
|
||||
cy.findAllByTestId('variables-input').last().click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Less than' }).click()
|
||||
cy.findAllByPlaceholderText('Type a value...').last().type('100')
|
||||
|
||||
cy.findAllByText('Equal to').last().click()
|
||||
|
||||
cy.findByTestId('variables-input').click()
|
||||
cy.findByRole('menuitem', { name: 'Age' }).click()
|
||||
cy.findByRole('button', { name: 'Select an operator' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Greater than' }).click()
|
||||
cy.findByPlaceholderText('Type a value...').type('20')
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your answer...')
|
||||
.type('15{enter}')
|
||||
getIframeBody().findByText('You are younger than 20').should('exist')
|
||||
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your answer...')
|
||||
.type('45{enter}')
|
||||
getIframeBody().findByText('You are older than 20').should('exist')
|
||||
|
||||
cy.findByRole('button', { name: 'Restart' }).click()
|
||||
getIframeBody()
|
||||
.findByPlaceholderText('Type your answer...')
|
||||
.type('90{enter}')
|
||||
getIframeBody().findByText('You are older than 80').should('exist')
|
||||
})
|
||||
})
|
@ -1,40 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('Redirect', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should redirect to URL correctly', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/logic/redirect.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByText('Configure...').click()
|
||||
cy.findByPlaceholderText('Type a URL...').type('google.com')
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Go to URL' }).click()
|
||||
cy.url().should('eq', 'https://www.google.com/')
|
||||
|
||||
cy.go('back')
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').as('open')
|
||||
})
|
||||
cy.findByText('Redirect to google.com').click()
|
||||
cy.findByRole('checkbox', { name: 'Open in new tab?' }).check({
|
||||
force: true,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByRole('button', { name: 'Go to URL' }).click()
|
||||
cy.get('@open').should(
|
||||
'have.been.calledOnceWithExactly',
|
||||
'https://google.com',
|
||||
'_blank'
|
||||
)
|
||||
})
|
||||
})
|
@ -1,33 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('Set variables', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it.only('options should work', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/logic/setVariable.json')
|
||||
cy.visit('/typebots/typebot4/edit')
|
||||
cy.findByText('Type a number...').click()
|
||||
cy.createVariable('Num')
|
||||
cy.findAllByText('Click to edit...').first().click()
|
||||
cy.createVariable('Total')
|
||||
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
|
||||
'1000 * {{Num}}',
|
||||
{ parseSpecialCharSequences: false }
|
||||
)
|
||||
cy.findAllByText('Click to edit...').last().click()
|
||||
cy.createVariable('Custom var')
|
||||
cy.findByRole('textbox', { name: 'Value / Expression:' }).type(
|
||||
'Custom value'
|
||||
)
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody().findByPlaceholderText('Type a number...').type('365{enter}')
|
||||
getIframeBody().findByText('Total: 365000').should('exist')
|
||||
getIframeBody().findByText('Custom var: Custom value')
|
||||
})
|
||||
})
|
@ -1,88 +0,0 @@
|
||||
import path from 'path'
|
||||
import { parse } from 'papaparse'
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
describe('Results page', () => {
|
||||
beforeEach(() => {
|
||||
prepareDbAndSignIn()
|
||||
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
|
||||
'getResults'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('results should be deletable', () => {
|
||||
cy.visit('/typebots/typebot2/results')
|
||||
cy.findByText('content198').should('exist')
|
||||
cy.findByText('content197').should('exist')
|
||||
cy.findAllByRole('checkbox').eq(2).check({ force: true })
|
||||
cy.findAllByRole('checkbox').eq(3).check({ force: true })
|
||||
cy.findByRole('button', { name: 'Delete 2' }).click({ force: true })
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findByText('content198').should('not.exist')
|
||||
cy.findByText('content197').should('not.exist')
|
||||
cy.wait(200)
|
||||
cy.findAllByRole('checkbox').first().check({ force: true })
|
||||
cy.findByRole('button', { name: 'Delete 198' }).click({ force: true })
|
||||
cy.findByRole('button', { name: 'Delete' }).click()
|
||||
cy.findAllByRole('row').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('submissions table should have infinite scroll', () => {
|
||||
cy.visit('/typebots/typebot2/results')
|
||||
cy.findByText('content50').should('not.exist')
|
||||
cy.findByText('content199').should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
cy.findByText('content149', { timeout: 10000 }).should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
cy.findByText('content99').should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
cy.findByText('content50').should('exist')
|
||||
cy.findByText('content0').should('exist')
|
||||
})
|
||||
|
||||
it('should correctly export selection in CSV', () => {
|
||||
cy.visit('/typebots/typebot2/results')
|
||||
cy.wait('@getResults')
|
||||
cy.findByRole('button', { name: 'Export' }).should('not.exist')
|
||||
cy.findByText('content199').should('exist')
|
||||
cy.findAllByRole('checkbox').eq(2).check({ force: true })
|
||||
cy.findAllByRole('checkbox').eq(3).check({ force: true })
|
||||
cy.findByRole('button', { name: 'Export 2' }).click({ force: true })
|
||||
const filename = path.join(
|
||||
downloadsFolder,
|
||||
`typebot-export_${new Date()
|
||||
.toLocaleDateString()
|
||||
.replaceAll('/', '-')}.csv`
|
||||
)
|
||||
cy.readFile(filename, { timeout: 15000 })
|
||||
.then(parse)
|
||||
.then(validateExportSelection as any)
|
||||
cy.findAllByRole('checkbox').first().check({ force: true })
|
||||
cy.findByRole('button', { name: 'Export 200' }).click({ force: true })
|
||||
const filenameAll = path.join(
|
||||
downloadsFolder,
|
||||
`typebot-export_${new Date()
|
||||
.toLocaleDateString()
|
||||
.replaceAll('/', '-')}_all.csv`
|
||||
)
|
||||
cy.readFile(filenameAll, { timeout: 15000 })
|
||||
.then(parse)
|
||||
.then(validateExportAll as any)
|
||||
})
|
||||
})
|
||||
|
||||
const validateExportSelection = (list: { data: unknown[][] }) => {
|
||||
expect(list.data, 'number of records').to.have.length(3)
|
||||
expect(list.data[1][1], 'first record').to.equal('content198')
|
||||
expect(list.data[2][1], 'second record').to.equal('content197')
|
||||
}
|
||||
|
||||
const validateExportAll = (list: { data: unknown[][] }) => {
|
||||
expect(list.data, 'number of records').to.have.length(201)
|
||||
expect(list.data[1][1], 'first record').to.equal('content199')
|
||||
expect(list.data[200][1], 'second record').to.equal('content0')
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('General settings', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/settings')
|
||||
getIframeBody()
|
||||
.findByRole('link', { name: 'Made with Typebot.' })
|
||||
.should('have.attr', 'href')
|
||||
.should('eq', 'https://www.typebot.io/?utm_source=litebadge')
|
||||
cy.findByRole('button', { name: 'General' }).click()
|
||||
cy.findByRole('checkbox', { name: 'Typebot.io branding' }).uncheck({
|
||||
force: true,
|
||||
})
|
||||
getIframeBody()
|
||||
.findByRole('link', { name: 'Made with Typebot.' })
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
@ -1,51 +0,0 @@
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
|
||||
const favIconUrl = 'https://www.baptistearno.com/favicon.png'
|
||||
const imageUrl = 'https://www.baptistearno.com/images/site-preview.png'
|
||||
|
||||
describe('Typing emulation', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/settings')
|
||||
cy.findByRole('button', { name: 'Metadata' }).click()
|
||||
|
||||
// Fav icon
|
||||
cy.findAllByRole('img', { name: 'Fav icon' })
|
||||
.click()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', '/favicon.png')
|
||||
cy.findByRole('button', { name: 'Giphy' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Embed link' }).click()
|
||||
cy.findByPlaceholderText('Paste the image link...').type(favIconUrl)
|
||||
cy.findAllByRole('img', { name: 'Fav icon' })
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', favIconUrl)
|
||||
|
||||
// Image
|
||||
cy.findAllByRole('img', { name: 'Website image' })
|
||||
.click()
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', '/viewer-preview.png')
|
||||
cy.findByRole('button', { name: 'Giphy' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Embed link' }).click()
|
||||
cy.findByPlaceholderText('Paste the image link...').type(imageUrl)
|
||||
cy.findAllByRole('img', { name: 'Website image' })
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', imageUrl)
|
||||
|
||||
// Title
|
||||
cy.findByRole('textbox', { name: 'Title:' })
|
||||
.click({ force: true })
|
||||
.clear()
|
||||
.type('Awesome typebot')
|
||||
|
||||
// Description
|
||||
cy.findByRole('textbox', { name: 'Description:' })
|
||||
.clear()
|
||||
.type('Lorem ipsum')
|
||||
})
|
||||
})
|
@ -1,20 +0,0 @@
|
||||
import { prepareDbAndSignIn, removePreventReload } from 'cypress/support'
|
||||
|
||||
describe('Typing emulation', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/settings')
|
||||
cy.findByRole('button', { name: 'Typing emulation' }).click()
|
||||
cy.findByTestId('speed').clear().type('350')
|
||||
cy.findByTestId('max-delay').clear().type('1.5')
|
||||
cy.findByRole('checkbox', { name: 'Typing emulation' }).uncheck({
|
||||
force: true,
|
||||
})
|
||||
cy.findByTestId('speed').should('not.exist')
|
||||
cy.findByTestId('max-delay').should('not.exist')
|
||||
})
|
||||
})
|
@ -1,98 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('General theme settings', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/theme')
|
||||
getIframeBody().findByText('Ready?').should('exist')
|
||||
cy.findByRole('button', { name: 'Chat' }).click()
|
||||
|
||||
// Host bubbles
|
||||
cy.findAllByRole('button', { name: 'Pick a color' }).first().click()
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#2a9d8f')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(1)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#ffffff')
|
||||
getIframeBody()
|
||||
.findByTestId('host-bubble')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(42, 157, 143)')
|
||||
getIframeBody()
|
||||
.findByTestId('host-bubble')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(255, 255, 255)')
|
||||
|
||||
// Buttons
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(4)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#7209b7')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(5)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#e9c46a')
|
||||
getIframeBody()
|
||||
.findByTestId('button')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(114, 9, 183)')
|
||||
getIframeBody()
|
||||
.findByTestId('button')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(233, 196, 106)')
|
||||
|
||||
// Guest bubbles
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(2)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#d8f3dc')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(3)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#264653')
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody()
|
||||
.findByTestId('guest-bubble')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(216, 243, 220)')
|
||||
getIframeBody()
|
||||
.findByTestId('guest-bubble')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(38, 70, 83)')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(3)
|
||||
.click({ force: true })
|
||||
|
||||
// Input
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(6)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' })
|
||||
.clear({ force: true })
|
||||
.type('#ffe8d6')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(7)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#023e8a')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(8)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('red')
|
||||
getIframeBody()
|
||||
.findByTestId('input')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(255, 232, 214)')
|
||||
getIframeBody()
|
||||
.findByTestId('input')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(2, 62, 138)')
|
||||
})
|
||||
})
|
@ -1,28 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('Custom CSS settings', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/theme')
|
||||
cy.findByRole('button', { name: 'Custom CSS' }).click()
|
||||
|
||||
cy.findByTestId('code-editor').type(
|
||||
'.typebot-button {background-color: green}',
|
||||
{
|
||||
parseSpecialCharSequences: false,
|
||||
}
|
||||
)
|
||||
getIframeBody()
|
||||
.findByTestId('button')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(0, 128, 0)')
|
||||
})
|
||||
})
|
@ -1,38 +0,0 @@
|
||||
import {
|
||||
getIframeBody,
|
||||
prepareDbAndSignIn,
|
||||
removePreventReload,
|
||||
} from 'cypress/support'
|
||||
|
||||
describe('General theme settings', () => {
|
||||
beforeEach(prepareDbAndSignIn)
|
||||
|
||||
afterEach(removePreventReload)
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.visit('/typebots/typebot4/theme')
|
||||
cy.findByRole('button', { name: 'General' }).click()
|
||||
|
||||
// Font
|
||||
cy.findByDisplayValue('Open Sans').clear().type('Roboto')
|
||||
cy.findByRole('menuitem', { name: 'Roboto Slab' }).click()
|
||||
getIframeBody()
|
||||
.findByTestId('container')
|
||||
.should('have.css', 'font-family')
|
||||
.should('eq', '"Roboto Slab"')
|
||||
|
||||
// BG color
|
||||
getIframeBody()
|
||||
.findByTestId('container')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgba(0, 0, 0, 0)')
|
||||
cy.findByDisplayValue('Color').check({ force: true })
|
||||
cy.findByRole('button', { name: 'Pick a color' }).click()
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#2a9d8f')
|
||||
getIframeBody()
|
||||
.findByTestId('container')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(42, 157, 143)')
|
||||
})
|
||||
})
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"types": ["cypress", "@testing-library/cypress", "cypress-file-upload"],
|
||||
"lib": ["es2015", "dom", "ES2021.String"],
|
||||
"target": "es5",
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
@ -7,7 +7,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "cypress run"
|
||||
"test": "yarn playwright test",
|
||||
"test:open": "PWDEBUG=1 yarn playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/css-reset": "^1.1.1",
|
||||
@ -16,8 +17,6 @@
|
||||
"@codemirror/lang-css": "^0.19.3",
|
||||
"@codemirror/lang-json": "^0.19.1",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@dnd-kit/core": "^4.0.3",
|
||||
"@dnd-kit/sortable": "^5.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@giphy/js-fetch-api": "^4.1.2",
|
||||
@ -72,6 +71,7 @@
|
||||
"utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.18.0",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"@types/google-spreadsheet": "^3.1.5",
|
||||
"@types/micro-cors": "^0.1.2",
|
||||
|
@ -9,6 +9,7 @@ import 'assets/styles/plate.css'
|
||||
import 'focus-visible/dist/focus-visible'
|
||||
import 'assets/styles/submissionsTable.css'
|
||||
import 'assets/styles/codeMirror.css'
|
||||
import 'assets/styles/custom.css'
|
||||
import { UserContext } from 'contexts/UserContext'
|
||||
import { TypebotContext } from 'contexts/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -56,7 +56,7 @@ if (process.env.NODE_ENV !== 'production')
|
||||
email: {
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
placeholder: 'email@email.com',
|
||||
placeholder: 'credentials@email.com',
|
||||
},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
|
@ -20,6 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
ownerId: user.id,
|
||||
parentFolderId,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
return res.send({ folders })
|
||||
}
|
||||
|
@ -3,17 +3,17 @@ import { Stack } from '@chakra-ui/layout'
|
||||
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { FolderContent } from 'components/dashboard/FolderContent'
|
||||
import { UserContext } from 'contexts/UserContext'
|
||||
import { TypebotDndContext } from 'contexts/TypebotDndContext'
|
||||
|
||||
const DashboardPage = () => {
|
||||
return (
|
||||
<UserContext>
|
||||
<Stack minH="100vh">
|
||||
<Seo title="My typebots" />
|
||||
<Stack>
|
||||
<DashboardHeader />
|
||||
<DashboardHeader />
|
||||
<TypebotDndContext>
|
||||
<FolderContent folder={null} />
|
||||
</Stack>
|
||||
</UserContext>
|
||||
</TypebotDndContext>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ const TypebotEditPage = () => (
|
||||
<Seo title="Editor" />
|
||||
<KBarProvider actions={actions}>
|
||||
<KBar />
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column" id="editor-container">
|
||||
<TypebotHeader />
|
||||
<Board />
|
||||
</Flex>
|
||||
|
@ -6,6 +6,7 @@ import { FolderContent } from 'components/dashboard/FolderContent'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useFolderContent } from 'services/folders'
|
||||
import { Spinner, useToast } from '@chakra-ui/react'
|
||||
import { TypebotDndContext } from 'contexts/TypebotDndContext'
|
||||
|
||||
const FolderPage = () => {
|
||||
const router = useRouter()
|
||||
@ -26,16 +27,18 @@ const FolderPage = () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack minH="100vh">
|
||||
<Seo title="My typebots" />
|
||||
<DashboardHeader />
|
||||
{!folder ? (
|
||||
<Flex flex="1">
|
||||
<Spinner mx="auto" />
|
||||
</Flex>
|
||||
) : (
|
||||
<FolderContent folder={folder} />
|
||||
)}
|
||||
<TypebotDndContext>
|
||||
{!folder ? (
|
||||
<Flex flex="1">
|
||||
<Spinner mx="auto" />
|
||||
</Flex>
|
||||
) : (
|
||||
<FolderContent folder={folder} />
|
||||
)}
|
||||
</TypebotDndContext>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
31
apps/builder/playwright.config.ts
Normal file
31
apps/builder/playwright.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { devices, PlaywrightTestConfig } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
globalSetup: require.resolve(path.join(__dirname, 'playwright/global-setup')),
|
||||
testDir: path.join(__dirname, 'playwright/tests'),
|
||||
timeout: 10 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
baseURL: process.env.NEXTAUTH_URL,
|
||||
trace: 'on-first-retry',
|
||||
storageState: path.join(__dirname, 'playwright/authenticatedState.json'),
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
outputDir: path.join(__dirname, 'playwright/test-results/'),
|
||||
projects: [
|
||||
{
|
||||
name: 'Chrome',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
export default config
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user