2
0

perf(e2e): ️ Migrate to Playwright

This commit is contained in:
Baptiste Arnaud
2022-01-28 09:42:31 +01:00
parent c5aaa323d1
commit 73f277fce7
145 changed files with 3104 additions and 2346 deletions

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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"

View File

@ -0,0 +1,29 @@
import {
ButtonProps,
IconButton,
Menu,
MenuButton,
MenuList,
} from '@chakra-ui/react'
import { MoreVerticalIcon } from 'assets/icons'
import { ReactNode } from 'react'
type Props = { children: ReactNode } & ButtonProps
export const MoreButton = ({ children, ...props }: Props) => {
return (
<Menu>
<MenuButton
as={IconButton}
icon={<MoreVerticalIcon />}
onMouseUp={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
colorScheme="gray"
variant="outline"
size="sm"
{...props}
/>
<MenuList>{children}</MenuList>
</Menu>
)
}

View File

@ -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

View File

@ -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>
)
}