2
0

🐛 (limits) Fix storage limit trigger and e2e tests

This commit is contained in:
Baptiste Arnaud
2022-09-24 08:58:23 +02:00
committed by Baptiste Arnaud
parent 1e26703ad4
commit 30dff2d5d7
52 changed files with 1024 additions and 2205 deletions

View File

@@ -2,7 +2,10 @@ import { Flex, Spinner, useDisclosure } from '@chakra-ui/react'
import { StatsCards } from 'components/analytics/StatsCards' import { StatsCards } from 'components/analytics/StatsCards'
import { Graph } from 'components/shared/Graph' import { Graph } from 'components/shared/Graph'
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
import { ChangePlanModal } from 'components/shared/modals/ChangePlanModal' import {
ChangePlanModal,
LimitReached,
} from 'components/shared/modals/ChangePlanModal'
import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext' import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { Stats } from 'models' import { Stats } from 'models'
@@ -49,7 +52,11 @@ export const AnalyticsContent = ({ stats }: { stats?: Stats }) => {
<Spinner color="gray" /> <Spinner color="gray" />
</Flex> </Flex>
)} )}
<ChangePlanModal onClose={onClose} isOpen={isOpen} /> <ChangePlanModal
onClose={onClose}
isOpen={isOpen}
type={LimitReached.ANALYTICS}
/>
<StatsCards stats={stats} pos="absolute" top={10} /> <StatsCards stats={stats} pos="absolute" top={10} />
</Flex> </Flex>
) )

View File

@@ -1,10 +1,12 @@
import { Button, HStack, Tag, useDisclosure, Text } from '@chakra-ui/react' import { Button, HStack, useDisclosure, Text } from '@chakra-ui/react'
import { FolderPlusIcon } from 'assets/icons' import { FolderPlusIcon } from 'assets/icons'
import { import {
LimitReached, LimitReached,
ChangePlanModal, ChangePlanModal,
} from 'components/shared/modals/ChangePlanModal' } from 'components/shared/modals/ChangePlanModal'
import { PlanTag } from 'components/shared/PlanTag'
import { useWorkspace } from 'contexts/WorkspaceContext' import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import React from 'react' import React from 'react'
import { isFreePlan } from 'services/workspace' import { isFreePlan } from 'services/workspace'
@@ -26,7 +28,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
> >
<HStack> <HStack>
<Text>Create a folder</Text> <Text>Create a folder</Text>
{isFreePlan(workspace) && <Tag colorScheme="orange">Pro</Tag>} {isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
</HStack> </HStack>
<ChangePlanModal <ChangePlanModal
isOpen={isOpen} isOpen={isOpen}

View File

@@ -7,7 +7,7 @@ export const useUsage = (workspaceId?: string) => {
{ totalChatsUsed: number; totalStorageUsed: number }, { totalChatsUsed: number; totalStorageUsed: number },
Error Error
>(workspaceId ? `/api/workspaces/${workspaceId}/usage` : null, fetcher, { >(workspaceId ? `/api/workspaces/${workspaceId}/usage` : null, fetcher, {
dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
}) })
return { return {
data, data,

View File

@@ -14,7 +14,7 @@ export const useInvoicesQuery = (stripeId?: string | null) => {
stripeId ? `/api/stripe/invoices?stripeId=${stripeId}` : null, stripeId ? `/api/stripe/invoices?stripeId=${stripeId}` : null,
fetcher, fetcher,
{ {
dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
} }
) )
return { return {

View File

@@ -24,7 +24,7 @@ export const MembersList = () => {
}) })
const handleDeleteMemberClick = (memberId: string) => async () => { const handleDeleteMemberClick = (memberId: string) => async () => {
if (!workspace || !members || !invitations) return if (!workspace) return
await deleteMember(workspace.id, memberId) await deleteMember(workspace.id, memberId)
mutate({ mutate({
members: members.filter((m) => m.userId !== memberId), members: members.filter((m) => m.userId !== memberId),
@@ -34,7 +34,7 @@ export const MembersList = () => {
const handleSelectNewRole = const handleSelectNewRole =
(memberId: string) => async (role: WorkspaceRole) => { (memberId: string) => async (role: WorkspaceRole) => {
if (!workspace || !members || !invitations) return if (!workspace) return
await updateMember(workspace.id, { userId: memberId, role }) await updateMember(workspace.id, { userId: memberId, role })
mutate({ mutate({
members: members.map((m) => members: members.map((m) =>
@@ -45,7 +45,7 @@ export const MembersList = () => {
} }
const handleDeleteInvitationClick = (id: string) => async () => { const handleDeleteInvitationClick = (id: string) => async () => {
if (!workspace || !members || !invitations) return if (!workspace) return
await deleteInvitation({ workspaceId: workspace.id, id }) await deleteInvitation({ workspaceId: workspace.id, id })
mutate({ mutate({
invitations: invitations.filter((i) => i.id !== id), invitations: invitations.filter((i) => i.id !== id),
@@ -55,7 +55,7 @@ export const MembersList = () => {
const handleSelectNewInvitationRole = const handleSelectNewInvitationRole =
(id: string) => async (type: WorkspaceRole) => { (id: string) => async (type: WorkspaceRole) => {
if (!workspace || !members || !invitations) return if (!workspace) return
await updateInvitation({ workspaceId: workspace.id, id, type }) await updateInvitation({ workspaceId: workspace.id, id, type })
mutate({ mutate({
invitations: invitations.map((i) => (i.id === id ? { ...i, type } : i)), invitations: invitations.map((i) => (i.id === id ? { ...i, type } : i)),
@@ -63,17 +63,15 @@ export const MembersList = () => {
}) })
} }
const handleNewInvitation = (invitation: WorkspaceInvitation) => { const handleNewInvitation = async (invitation: WorkspaceInvitation) => {
if (!members || !invitations) return await mutate({
mutate({
members, members,
invitations: [...invitations, invitation], invitations: [...invitations, invitation],
}) })
} }
const handleNewMember = (member: Member) => { const handleNewMember = async (member: Member) => {
if (!members || !invitations) return await mutate({
mutate({
members: [...members, member], members: [...members, member],
invitations, invitations,
}) })
@@ -81,7 +79,7 @@ export const MembersList = () => {
const canInviteNewMember = checkCanInviteMember({ const canInviteNewMember = checkCanInviteMember({
plan: workspace?.plan, plan: workspace?.plan,
currentMembersCount: [...(members ?? []), ...(invitations ?? [])].length, currentMembersCount: [...members, ...invitations].length,
}) })
return ( return (
@@ -103,7 +101,7 @@ export const MembersList = () => {
isLocked={!canInviteNewMember} isLocked={!canInviteNewMember}
/> />
)} )}
{members?.map((member) => ( {members.map((member) => (
<MemberItem <MemberItem
key={member.userId} key={member.userId}
email={member.email ?? ''} email={member.email ?? ''}
@@ -116,7 +114,7 @@ export const MembersList = () => {
canEdit={canEdit} canEdit={canEdit}
/> />
))} ))}
{invitations?.map((invitation) => ( {invitations.map((invitation) => (
<MemberItem <MemberItem
key={invitation.email} key={invitation.email}
email={invitation.email ?? ''} email={invitation.email ?? ''}

View File

@@ -1,5 +1,7 @@
import { HStack, Tag, Text, Tooltip } from '@chakra-ui/react' import { HStack, Text, Tooltip } from '@chakra-ui/react'
import { PlanTag } from 'components/shared/PlanTag'
import { useWorkspace } from 'contexts/WorkspaceContext' import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import { import {
BubbleBlockType, BubbleBlockType,
InputBlockType, InputBlockType,
@@ -52,11 +54,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
<Tooltip label="Upload Files"> <Tooltip label="Upload Files">
<HStack> <HStack>
<Text>File</Text> <Text>File</Text>
{isFreePlan(workspace) && ( {isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
<Tag colorScheme="orange" size="sm">
Pro
</Tag>
)}
</HStack> </HStack>
</Tooltip> </Tooltip>
) )

View File

@@ -1,14 +1,12 @@
import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react'
import { import {
Flex, ChangePlanModal,
FormLabel, LimitReached,
Stack, } from 'components/shared/modals/ChangePlanModal'
Switch, import { PlanTag } from 'components/shared/PlanTag'
Tag,
useDisclosure,
} from '@chakra-ui/react'
import { ChangePlanModal } from 'components/shared/modals/ChangePlanModal'
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
import { useWorkspace } from 'contexts/WorkspaceContext' import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import { GeneralSettings } from 'models' import { GeneralSettings } from 'models'
import React from 'react' import React from 'react'
import { isFreePlan } from 'services/workspace' import { isFreePlan } from 'services/workspace'
@@ -25,9 +23,9 @@ export const GeneralSettingsForm = ({
}: Props) => { }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const isUserFreePlan = isFreePlan(workspace) const isWorkspaceFreePlan = isFreePlan(workspace)
const handleSwitchChange = () => { const handleSwitchChange = () => {
if (generalSettings?.isBrandingEnabled && isUserFreePlan) return if (generalSettings?.isBrandingEnabled && isWorkspaceFreePlan) return
onGeneralSettingsChange({ onGeneralSettingsChange({
...generalSettings, ...generalSettings,
isBrandingEnabled: !generalSettings?.isBrandingEnabled, isBrandingEnabled: !generalSettings?.isBrandingEnabled,
@@ -56,15 +54,19 @@ export const GeneralSettingsForm = ({
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
<ChangePlanModal isOpen={isOpen} onClose={onClose} /> <ChangePlanModal
isOpen={isOpen}
onClose={onClose}
type={LimitReached.BRAND}
/>
<Flex <Flex
justifyContent="space-between" justifyContent="space-between"
align="center" align="center"
onClick={isUserFreePlan ? onOpen : undefined} onClick={isWorkspaceFreePlan ? onOpen : undefined}
> >
<FormLabel htmlFor="branding" mb="0"> <FormLabel htmlFor="branding" mb="0">
Typebot.io branding{' '} Typebot.io branding{' '}
{isUserFreePlan && <Tag colorScheme="orange">Pro</Tag>} {isWorkspaceFreePlan && <PlanTag plan={Plan.STARTER} />}
</FormLabel> </FormLabel>
<Switch <Switch
id="branding" id="branding"

View File

@@ -36,11 +36,10 @@ export const EditableUrl = ({
<Tooltip label="Edit"> <Tooltip label="Edit">
<EditablePreview <EditablePreview
mx={1} mx={1}
bgColor="blue.500" borderWidth="1px"
color="white"
px={3} px={3}
rounded="md" rounded="md"
cursor="pointer" cursor="text"
display="flex" display="flex"
fontWeight="semibold" fontWeight="semibold"
/> />

View File

@@ -4,18 +4,20 @@ import {
HStack, HStack,
IconButton, IconButton,
Stack, Stack,
Tag,
Wrap, Wrap,
Text, Text,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons' import { TrashIcon } from 'assets/icons'
import { UpgradeButton } from 'components/shared/buttons/UpgradeButton' import { UpgradeButton } from 'components/shared/buttons/UpgradeButton'
import { useToast } from 'components/shared/hooks/useToast' import { useToast } from 'components/shared/hooks/useToast'
import { LimitReached } from 'components/shared/modals/ChangePlanModal'
import { PlanTag } from 'components/shared/PlanTag'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { useWorkspace } from 'contexts/WorkspaceContext' import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import React from 'react' import React from 'react'
import { parseDefaultPublicId } from 'services/typebots' import { parseDefaultPublicId } from 'services/typebots'
import { isFreePlan } from 'services/workspace' import { isWorkspaceProPlan } from 'services/workspace'
import { getViewerUrl, isDefined, isNotDefined } from 'utils' import { getViewerUrl, isDefined, isNotDefined } from 'utils'
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown' import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
import { EditableUrl } from './EditableUrl' import { EditableUrl } from './EditableUrl'
@@ -80,19 +82,18 @@ export const ShareContent = () => {
/> />
</HStack> </HStack>
)} )}
{isFreePlan(workspace) ? ( {isWorkspaceProPlan(workspace) &&
<UpgradeButton colorScheme="gray"> isNotDefined(typebot?.customDomain) ? (
<Text mr="2">Add my domain</Text>{' '} <CustomDomainsDropdown
<Tag colorScheme="orange">Pro</Tag> onCustomDomainSelect={handleCustomDomainChange}
</UpgradeButton> />
) : ( ) : (
<> <UpgradeButton
{isNotDefined(typebot?.customDomain) && ( colorScheme="gray"
<CustomDomainsDropdown limitReachedType={LimitReached.CUSTOM_DOMAIN}
onCustomDomainSelect={handleCustomDomainChange} >
/> <Text mr="2">Add my domain</Text> <PlanTag plan={Plan.PRO} />
)} </UpgradeButton>
</>
)} )}
</Stack> </Stack>

View File

@@ -9,7 +9,7 @@ import {
computeSourceCoordinates, computeSourceCoordinates,
computeDropOffPath, computeDropOffPath,
} from 'services/graph' } from 'services/graph'
import { isFreePlan } from 'services/workspace' import { isWorkspaceProPlan } from 'services/workspace'
import { byId, isDefined } from 'utils' import { byId, isDefined } from 'utils'
type Props = { type Props = {
@@ -28,7 +28,7 @@ export const DropOffEdge = ({
const { sourceEndpoints, graphPosition } = useGraph() const { sourceEndpoints, graphPosition } = useGraph()
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const isUserOnFreePlan = isFreePlan(workspace) const isProPlan = isWorkspaceProPlan(workspace)
const totalAnswers = useMemo( const totalAnswers = useMemo(
() => answersCounts.find((a) => a.groupId === groupId)?.totalAnswers, () => answersCounts.find((a) => a.groupId === groupId)?.totalAnswers,
@@ -95,7 +95,7 @@ export const DropOffEdge = ({
> >
<Tooltip <Tooltip
label="Unlock Drop-off rate by upgrading to Pro plan" label="Unlock Drop-off rate by upgrading to Pro plan"
isDisabled={!isUserOnFreePlan} isDisabled={isProPlan}
> >
<VStack <VStack
bgColor={'red.500'} bgColor={'red.500'}
@@ -105,13 +105,28 @@ export const DropOffEdge = ({
justifyContent="center" justifyContent="center"
w="full" w="full"
h="full" h="full"
filter={isUserOnFreePlan ? 'blur(4px)' : ''} onClick={isProPlan ? undefined : onUnlockProPlanClick}
onClick={isUserOnFreePlan ? onUnlockProPlanClick : undefined} cursor={isProPlan ? 'auto' : 'pointer'}
cursor={isUserOnFreePlan ? 'pointer' : 'auto'}
> >
<Text>{isUserOnFreePlan ? 'X' : dropOffRate}%</Text> <Text filter={isProPlan ? '' : 'blur(2px)'}>
{isProPlan ? (
dropOffRate
) : (
<Text as="span" filter="blur(2px)">
X
</Text>
)}
%
</Text>
<Tag colorScheme="red"> <Tag colorScheme="red">
{isUserOnFreePlan ? 'n' : totalDroppedUser} users {isProPlan ? (
totalDroppedUser
) : (
<Text as="span" filter="blur(3px)" mr="1">
NN
</Text>
)}{' '}
users
</Tag> </Tag>
</VStack> </VStack>
</Tooltip> </Tooltip>

View File

@@ -25,7 +25,6 @@ export const Edges = ({
pos="absolute" pos="absolute"
left="0" left="0"
top="0" top="0"
pointerEvents="none"
shapeRendering="geometricPrecision" shapeRendering="geometricPrecision"
> >
<DrawingEdge /> <DrawingEdge />

View File

@@ -1,12 +1,12 @@
import { Tag } from '@chakra-ui/react' import { Tag, TagProps } from '@chakra-ui/react'
import { Plan } from 'db' import { Plan } from 'db'
export const PlanTag = ({ plan }: { plan?: Plan }) => { export const PlanTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => {
switch (plan) { switch (plan) {
case Plan.LIFETIME: case Plan.LIFETIME:
case Plan.PRO: { case Plan.PRO: {
return ( return (
<Tag colorScheme="blue" data-testid="plan-tag"> <Tag colorScheme="blue" data-testid="pro-plan-tag" {...props}>
Pro Pro
</Tag> </Tag>
) )
@@ -14,14 +14,14 @@ export const PlanTag = ({ plan }: { plan?: Plan }) => {
case Plan.OFFERED: case Plan.OFFERED:
case Plan.STARTER: { case Plan.STARTER: {
return ( return (
<Tag colorScheme="orange" data-testid="plan-tag"> <Tag colorScheme="orange" data-testid="starter-plan-tag" {...props}>
Starter Starter
</Tag> </Tag>
) )
} }
default: { default: {
return ( return (
<Tag colorScheme="gray" data-testid="plan-tag"> <Tag colorScheme="gray" data-testid="free-plan-tag" {...props}>
Free Free
</Tag> </Tag>
) )

View File

@@ -5,9 +5,9 @@ import { isNotDefined } from 'utils'
import { ChangePlanModal } from '../modals/ChangePlanModal' import { ChangePlanModal } from '../modals/ChangePlanModal'
import { LimitReached } from '../modals/ChangePlanModal' import { LimitReached } from '../modals/ChangePlanModal'
type Props = { type?: LimitReached } & ButtonProps type Props = { limitReachedType?: LimitReached } & ButtonProps
export const UpgradeButton = ({ type, ...props }: Props) => { export const UpgradeButton = ({ limitReachedType, ...props }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
return ( return (
@@ -18,7 +18,11 @@ export const UpgradeButton = ({ type, ...props }: Props) => {
onClick={onOpen} onClick={onOpen}
> >
{props.children ?? 'Upgrade'} {props.children ?? 'Upgrade'}
<ChangePlanModal isOpen={isOpen} onClose={onClose} type={type} /> <ChangePlanModal
isOpen={isOpen}
onClose={onClose}
type={limitReachedType}
/>
</Button> </Button>
) )
} }

View File

@@ -13,9 +13,10 @@ import { ChangePlanForm } from 'components/shared/ChangePlanForm'
export enum LimitReached { export enum LimitReached {
BRAND = 'remove branding', BRAND = 'remove branding',
CUSTOM_DOMAIN = 'add custom domain', CUSTOM_DOMAIN = 'add custom domains',
FOLDER = 'create folders', FOLDER = 'create folders',
FILE_INPUT = 'use file input blocks', FILE_INPUT = 'use file input blocks',
ANALYTICS = 'unlock in-depth analytics',
} }
type ChangePlanModalProps = { type ChangePlanModalProps = {

View File

@@ -37,7 +37,11 @@ const workspaceContext = createContext<{
//@ts-ignore //@ts-ignore
}>({}) }>({})
export const WorkspaceContext = ({ children }: { children: ReactNode }) => { type WorkspaceContextProps = {
children: ReactNode
}
export const WorkspaceContext = ({ children }: WorkspaceContextProps) => {
const { query } = useRouter() const { query } = useRouter()
const { user } = useUser() const { user } = useUser()
const userId = user?.id const userId = user?.id
@@ -45,6 +49,7 @@ export const WorkspaceContext = ({ children }: { children: ReactNode }) => {
const { workspaces, isLoading, mutate } = useWorkspaces({ userId }) const { workspaces, isLoading, mutate } = useWorkspaces({ userId })
const [currentWorkspace, setCurrentWorkspace] = const [currentWorkspace, setCurrentWorkspace] =
useState<WorkspaceWithMembers>() useState<WorkspaceWithMembers>()
const [pendingWorkspaceId, setPendingWorkspaceId] = useState<string>()
const canEdit = const canEdit =
workspaces workspaces
@@ -58,7 +63,9 @@ export const WorkspaceContext = ({ children }: { children: ReactNode }) => {
useEffect(() => { useEffect(() => {
if (!workspaces || workspaces.length === 0 || currentWorkspace) return if (!workspaces || workspaces.length === 0 || currentWorkspace) return
const lastWorspaceId = const lastWorspaceId =
query.workspaceId?.toString() ?? localStorage.getItem('workspaceId') pendingWorkspaceId ??
query.workspaceId?.toString() ??
localStorage.getItem('workspaceId')
const defaultWorkspace = lastWorspaceId const defaultWorkspace = lastWorspaceId
? workspaces.find(byId(lastWorspaceId)) ? workspaces.find(byId(lastWorspaceId))
: workspaces.find((w) => : workspaces.find((w) =>
@@ -77,11 +84,8 @@ export const WorkspaceContext = ({ children }: { children: ReactNode }) => {
}, [currentWorkspace?.id]) }, [currentWorkspace?.id])
useEffect(() => { useEffect(() => {
if ( if (!currentWorkspace) return setPendingWorkspaceId(typebot?.workspaceId)
!typebot?.workspaceId || if (!typebot?.workspaceId || typebot.workspaceId === currentWorkspace.id)
!currentWorkspace ||
typebot.workspaceId === currentWorkspace.id
)
return return
switchWorkspace(typebot.workspaceId) switchWorkspace(typebot.workspaceId)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -38,6 +38,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
userId: existingUser.id, userId: existingUser.id,
}, },
}) })
if (env('E2E_TEST') !== 'true')
await sendEmailNotification({
to: data.email,
subject: "You've been invited to collaborate 🤝",
html: workspaceMemberInvitationEmail({
workspaceName: workspace.name,
guestEmail: data.email,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
hostEmail: user.email ?? '',
}),
})
return res.send({ return res.send({
member: { member: {
userId: existingUser.id, userId: existingUser.id,
@@ -47,19 +58,21 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
}, },
}) })
} else await prisma.workspaceInvitation.create({ data }) } else {
if (env('E2E_TEST') !== 'true') const invitation = await prisma.workspaceInvitation.create({ data })
await sendEmailNotification({ if (env('E2E_TEST') !== 'true')
to: data.email, await sendEmailNotification({
subject: "You've been invited to collaborate 🤝", to: data.email,
html: workspaceMemberInvitationEmail({ subject: "You've been invited to collaborate 🤝",
workspaceName: workspace.name, html: workspaceMemberInvitationEmail({
guestEmail: data.email, workspaceName: workspace.name,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`, guestEmail: data.email,
hostEmail: user.email ?? '', url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
}), hostEmail: user.email ?? '',
}) }),
return res.send({ message: 'success' }) })
return res.send({ invitation })
}
} }
methodNotAllowed(res) methodNotAllowed(res)
} }

View File

@@ -6,7 +6,7 @@ export const refreshUser = async () => {
document.dispatchEvent(event) document.dispatchEvent(event)
} }
export const connectedAsOtherUser = async (page: Page) => export const mockSessionResponsesToOtherUser = async (page: Page) =>
page.route('/api/auth/session', (route) => { page.route('/api/auth/session', (route) => {
if (route.request().method() === 'GET') { if (route.request().method() === 'GET') {
return route.fulfill({ return route.fulfill({

View File

@@ -21,6 +21,7 @@ import { readFileSync } from 'fs'
import { injectFakeResults } from 'utils' import { injectFakeResults } from 'utils'
import { encrypt } from 'utils/api' import { encrypt } from 'utils/api'
import Stripe from 'stripe' import Stripe from 'stripe'
import cuid from 'cuid'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@@ -28,7 +29,7 @@ const stripe = new Stripe(process.env.STRIPE_TEST_SECRET_KEY ?? '', {
apiVersion: '2022-08-01', apiVersion: '2022-08-01',
}) })
const userId = 'userId' export const userId = 'userId'
const otherUserId = 'otherUserId' const otherUserId = 'otherUserId'
export const freeWorkspaceId = 'freeWorkspace' export const freeWorkspaceId = 'freeWorkspace'
export const starterWorkspaceId = 'starterWorkspace' export const starterWorkspaceId = 'starterWorkspace'
@@ -49,6 +50,12 @@ export const teardownDatabase = async () => {
return prisma.webhook.deleteMany() return prisma.webhook.deleteMany()
} }
export const deleteWorkspaces = async (workspaceIds: string[]) => {
await prisma.workspace.deleteMany({
where: { id: { in: workspaceIds } },
})
}
export const addSubscriptionToWorkspace = async ( export const addSubscriptionToWorkspace = async (
workspaceId: string, workspaceId: string,
items: Stripe.SubscriptionCreateParams.Item[], items: Stripe.SubscriptionCreateParams.Item[],
@@ -90,12 +97,12 @@ export const addSubscriptionToWorkspace = async (
} }
export const setupDatabase = async () => { export const setupDatabase = async () => {
await createWorkspaces() await setupWorkspaces()
await createUsers() await setupUsers()
return createCredentials() return setupCredentials()
} }
export const createWorkspaces = async () => export const setupWorkspaces = async () =>
prisma.workspace.createMany({ prisma.workspace.createMany({
data: [ data: [
{ {
@@ -122,21 +129,27 @@ export const createWorkspaces = async () =>
], ],
}) })
export const createWorkspace = async (workspace: Partial<Workspace>) => { export const createWorkspaces = async (workspaces: Partial<Workspace>[]) => {
const { id: workspaceId } = await prisma.workspace.create({ const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid())
data: { await prisma.workspace.createMany({
data: workspaces.map((workspace, index) => ({
id: workspaceIds[index],
name: 'Free workspace', name: 'Free workspace',
plan: Plan.FREE, plan: Plan.FREE,
...workspace, ...workspace,
}, })),
}) })
await prisma.memberInWorkspace.create({ await prisma.memberInWorkspace.createMany({
data: { userId, workspaceId, role: WorkspaceRole.ADMIN }, data: workspaces.map((_, index) => ({
userId,
workspaceId: workspaceIds[index],
role: WorkspaceRole.ADMIN,
})),
}) })
return workspaceId return workspaceIds
} }
export const createUsers = async () => { export const setupUsers = async () => {
await prisma.user.create({ await prisma.user.create({
data: { data: {
id: userId, id: userId,
@@ -237,7 +250,7 @@ export const createFolders = (partialFolders: Partial<DashboardFolder>[]) =>
})), })),
}) })
const createCredentials = () => { const setupCredentials = () => {
const { encryptedData, iv } = encrypt({ const { encryptedData, iv } = encrypt({
expiry_date: 1642441058842, expiry_date: 1642441058842,
access_token: access_token:

View File

@@ -1,5 +1,8 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import path from 'path' import path from 'path'
import { userId } from 'playwright/services/database'
test.describe.configure({ mode: 'parallel' })
test('should display user info properly', async ({ page }) => { test('should display user info properly', async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')
@@ -18,7 +21,7 @@ test('should display user info properly', async ({ page }) => {
await expect(page.locator('img >> nth=1')).toHaveAttribute( await expect(page.locator('img >> nth=1')).toHaveAttribute(
'src', 'src',
new RegExp( new RegExp(
`http://localhost:9000/typebot/public/users/proUser/avatar`, `http://localhost:9000/typebot/public/users/${userId}/avatar`,
'gm' 'gm'
) )
) )

View File

@@ -0,0 +1,30 @@
import test, { expect } from '@playwright/test'
import cuid from 'cuid'
import path from 'path'
import {
importTypebotInDatabase,
starterWorkspaceId,
} from '../services/database'
test('analytics are not available for non-pro workspaces', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/results/submissionHeader.json'),
{
id: typebotId,
workspaceId: starterWorkspaceId,
}
)
await page.goto(`/typebots/${typebotId}/results/analytics`)
const firstDropoffBox = page.locator('text="%" >> nth=0')
await firstDropoffBox.hover()
await expect(
page.locator('text="Unlock Drop-off rate by upgrading to Pro plan"')
).toBeVisible()
await firstDropoffBox.click()
await expect(
page.locator(
'text="You need to upgrade your plan in order to unlock in-depth analytics"'
)
).toBeVisible()
})

View File

@@ -5,13 +5,34 @@ import {
addSubscriptionToWorkspace, addSubscriptionToWorkspace,
createResults, createResults,
createTypebots, createTypebots,
createWorkspace, createWorkspaces,
starterWorkspaceId, deleteWorkspaces,
} from '../services/database' } from '../services/database'
const usageWorkspaceId = cuid()
const usageTypebotId = cuid()
const planChangeWorkspaceId = cuid()
test.beforeAll(async () => {
await createWorkspaces([
{
id: usageWorkspaceId,
name: 'Usage Workspace',
plan: Plan.STARTER,
},
{
id: planChangeWorkspaceId,
name: 'Plan Change Workspace',
},
])
await createTypebots([{ id: usageTypebotId, workspaceId: usageWorkspaceId }])
})
test.afterAll(async () => {
await deleteWorkspaces([usageWorkspaceId, planChangeWorkspaceId])
})
test('should display valid usage', async ({ page }) => { test('should display valid usage', async ({ page }) => {
const starterTypebotId = cuid()
createTypebots([{ id: starterTypebotId, workspaceId: starterWorkspaceId }])
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
@@ -24,37 +45,34 @@ test('should display valid usage', async ({ page }) => {
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await expect(page.locator('text="/ 300"')).toBeVisible() await expect(page.locator('text="/ 300"')).toBeVisible()
await expect(page.locator('text="Storage"')).toBeHidden()
await page.click('text=Free workspace', { force: true }) await page.click('text=Free workspace', { force: true })
await createResults({ await createResults({
idPrefix: 'usage',
count: 10, count: 10,
typebotId: starterTypebotId, typebotId: usageTypebotId,
isChronological: false,
fakeStorage: 1100 * 1024 * 1024, fakeStorage: 1100 * 1024 * 1024,
}) })
await page.click('text=Free workspace') await page.click('text=Free workspace')
await page.click('text="Starter workspace"') await page.click('text="Usage Workspace"')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await expect(page.locator('text="/ 2,000"')).toBeVisible() await expect(page.locator('text="/ 2,000"')).toBeVisible()
await expect(page.locator('text="/ 2 GB"')).toBeVisible() await expect(page.locator('text="/ 2 GB"')).toBeVisible()
await expect(page.locator('text="1.07 GB"')).toBeVisible() await expect(page.locator('text="10" >> nth=0')).toBeVisible()
await expect(page.locator('text="200"')).toBeVisible()
await expect(page.locator('[role="progressbar"] >> nth=0')).toHaveAttribute( await expect(page.locator('[role="progressbar"] >> nth=0')).toHaveAttribute(
'aria-valuenow', 'aria-valuenow',
'10' '1'
) )
await expect(page.locator('text="1.07 GB"')).toBeVisible()
await expect(page.locator('[role="progressbar"] >> nth=1')).toHaveAttribute( await expect(page.locator('[role="progressbar"] >> nth=1')).toHaveAttribute(
'aria-valuenow', 'aria-valuenow',
'54' '54'
) )
await createResults({ await createResults({
idPrefix: 'usage2', typebotId: usageTypebotId,
typebotId: starterTypebotId, count: 1090,
isChronological: false,
count: 900,
fakeStorage: 1200 * 1024 * 1024, fakeStorage: 1200 * 1024 * 1024,
}) })
await page.click('text="Settings"') await page.click('text="Settings"')
@@ -68,12 +86,11 @@ test('should display valid usage', async ({ page }) => {
}) })
test('plan changes should work', async ({ page }) => { test('plan changes should work', async ({ page }) => {
const workspaceId = await createWorkspace({ name: 'Awesome workspace' }) test.setTimeout(80000)
// Upgrade to STARTER // Upgrade to STARTER
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Pro workspace') await page.click('text=Pro workspace')
await page.click('text=Awesome workspace') await page.click('text=Plan Change Workspace')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await page.click('button >> text="2,000"') await page.click('button >> text="2,000"')
@@ -89,7 +106,7 @@ test('plan changes should work', async ({ page }) => {
await expect(page.locator('text=$4.00 >> nth=0')).toBeVisible() await expect(page.locator('text=$4.00 >> nth=0')).toBeVisible()
await expect(page.locator('text=user@email.com')).toBeVisible() await expect(page.locator('text=user@email.com')).toBeVisible()
await addSubscriptionToWorkspace( await addSubscriptionToWorkspace(
workspaceId, planChangeWorkspaceId,
[ [
{ {
price: process.env.STRIPE_STARTER_PRICE_ID, price: process.env.STRIPE_STARTER_PRICE_ID,
@@ -148,26 +165,23 @@ test('plan changes should work', async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await expect(page.locator('[data-testid="plan-tag"]')).toHaveText('Pro') await expect(page.locator('[data-testid="pro-plan-tag"]')).toBeVisible()
await page.click('button >> text="Cancel my subscription"') await page.click('button >> text="Cancel my subscription"')
await expect(page.locator('[data-testid="plan-tag"]')).toHaveText('Free') await expect(page.locator('[data-testid="free-plan-tag"]')).toBeVisible()
}) })
test('should display invoices', async ({ page }) => { test('should display invoices', async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await expect( await expect(page.locator('text="Invoices"')).toBeHidden()
page.locator('text="No invoices found for this workspace."')
).toBeVisible()
await page.click('text=Pro workspace', { force: true }) await page.click('text=Pro workspace', { force: true })
await page.click('text=Pro workspace') await page.click('text=Pro workspace')
await page.click('text=Starter workspace') await page.click('text=Plan Change Workspace')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await page.click('text=Billing & Usage') await page.click('text=Billing & Usage')
await expect(page.locator('text="Invoices"')).toBeVisible() await expect(page.locator('text="Invoices"')).toBeVisible()
await expect(page.locator('text="Wed Jun 01 2022"')).toBeVisible() await expect(page.locator('tr')).toHaveCount(2)
await expect(page.locator('text="74567541-0001"')).toBeVisible() await expect(page.locator('text="€39.00"')).toBeVisible()
await expect(page.locator('text="€30.00" >> nth=0')).toBeVisible()
}) })

View File

@@ -8,6 +8,7 @@ import {
createResults, createResults,
createTypebots, createTypebots,
parseDefaultGroupWithBlock, parseDefaultGroupWithBlock,
userId,
} from '../services/database' } from '../services/database'
test.describe('Typebot owner', () => { test.describe('Typebot owner', () => {
@@ -21,7 +22,7 @@ test.describe('Typebot owner', () => {
plan: Plan.FREE, plan: Plan.FREE,
members: { members: {
createMany: { createMany: {
data: [{ role: WorkspaceRole.ADMIN, userId: 'proUser' }], data: [{ role: WorkspaceRole.ADMIN, userId }],
}, },
}, },
}, },
@@ -51,20 +52,20 @@ test.describe('Typebot owner', () => {
await expect(page.locator('text=Free user')).toBeHidden() await expect(page.locator('text=Free user')).toBeHidden()
await page.fill( await page.fill(
'input[placeholder="colleague@company.com"]', 'input[placeholder="colleague@company.com"]',
'free-user@email.com' 'other-user@email.com'
) )
await page.click('text=Can edit') await page.click('text=Can edit')
await page.click('text=Can view') await page.click('text=Can view')
await page.click('text=Invite') await page.click('text=Invite')
await expect(page.locator('text=Free user')).toBeVisible() await expect(page.locator('text=James Doe')).toBeVisible()
await page.click('text="guest@email.com"') await page.click('text="guest@email.com"')
await page.click('text="Remove"') await page.click('text="Remove"')
await expect(page.locator('text="guest@email.com"')).toBeHidden() await expect(page.locator('text="guest@email.com"')).toBeHidden()
}) })
}) })
test.describe('Collaborator', () => { test.describe('Guest', () => {
test('should display shared typebots', async ({ page }) => { test('should have shared typebots displayed', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()
const guestWorkspaceId = cuid() const guestWorkspaceId = cuid()
await prisma.workspace.create({ await prisma.workspace.create({
@@ -74,7 +75,7 @@ test.describe('Collaborator', () => {
plan: Plan.FREE, plan: Plan.FREE,
members: { members: {
createMany: { createMany: {
data: [{ role: WorkspaceRole.GUEST, userId: 'proUser' }], data: [{ role: WorkspaceRole.GUEST, userId }],
}, },
}, },
}, },
@@ -89,29 +90,34 @@ test.describe('Collaborator', () => {
options: defaultTextInputOptions, options: defaultTextInputOptions,
}), }),
}, },
{
name: 'Another typebot',
workspaceId: guestWorkspaceId,
},
]) ])
await prisma.collaboratorsOnTypebots.create({ await prisma.collaboratorsOnTypebots.create({
data: { data: {
typebotId, typebotId,
userId: 'proUser', userId,
type: CollaborationType.READ, type: CollaborationType.READ,
}, },
}) })
await createFolder(guestWorkspaceId, 'Guest folder') await createFolder(guestWorkspaceId, 'Guest folder')
await createResults({ typebotId, count: 10 }) await createResults({ typebotId, count: 10 })
await page.goto(`/typebots`) await page.goto(`/typebots`)
await page.click("text=Pro user's workspace") await page.click('text=Pro workspace')
await page.click('text=Guest workspace #2') await page.click('text=Guest workspace #2')
await expect(page.locator('text=Guest typebot')).toBeVisible() await expect(page.locator('text=Guest typebot')).toBeVisible()
await expect(page.locator('text=Another typebot')).toBeHidden()
await expect(page.locator('text=Guest folder')).toBeHidden() await expect(page.locator('text=Guest folder')).toBeHidden()
await page.click('text=Guest typebot') await page.click('text=Guest typebot')
await page.click('button[aria-label="Show collaboration menu"]') await page.click('button[aria-label="Show collaboration menu"]')
await page.click('text=Everyone at Guest workspace') await page.click('text=Everyone at Guest workspace')
await expect(page.locator('text="Remove"')).toBeHidden() await expect(page.locator('text="Remove"')).toBeHidden()
await expect(page.locator('text=Pro user')).toBeVisible() await expect(page.locator('text=John Doe')).toBeVisible()
await page.click('text=Group #1', { force: true }) await page.click('text=Group #1', { force: true })
await expect(page.locator('input[value="Group #1"]')).toBeHidden() await expect(page.locator('input[value="Group #1"]')).toBeHidden()
await page.goto(`/typebots/${typebotId}/results`) await page.goto(`/typebots/${typebotId}/results`)
await expect(page.locator('text="See logs" >> nth=10')).toBeVisible() await expect(page.locator('text="See logs" >> nth=9')).toBeVisible()
}) })
}) })

View File

@@ -59,6 +59,10 @@ test.describe('Starter workspace', () => {
await page.goto(`/typebots/${typebotId}/share`) await page.goto(`/typebots/${typebotId}/share`)
await expect(page.locator('text=Pro')).toBeVisible() await expect(page.locator('text=Pro')).toBeVisible()
await page.click('text=Add my domain') await page.click('text=Add my domain')
await expect(page.locator('text=For solo creator')).toBeVisible() await expect(
page.locator(
'text="You need to upgrade your plan in order to add custom domains"'
)
).toBeVisible()
}) })
}) })

View File

@@ -1,88 +1,87 @@
import test, { expect, Page } from '@playwright/test' import test, { expect, Page } from '@playwright/test'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path'
import { createFolders, createTypebots } from '../services/database' import { createFolders, createTypebots } from '../services/database'
import { deleteButtonInConfirmDialog } from '../services/selectorUtils' import { deleteButtonInConfirmDialog } from '../services/selectorUtils'
test.describe('Dashboard page', () => { test('folders navigation should work', async ({ page }) => {
test('folders navigation should work', async ({ page }) => { await page.goto('/typebots')
const createFolderButton = page.locator('button:has-text("Create a folder")')
await expect(createFolderButton).not.toBeDisabled()
await createFolderButton.click()
await page.click('text="New folder"')
await page.fill('input[value="New folder"]', 'My folder #1')
await page.press('input[value="My folder #1"]', 'Enter')
await waitForNextApiCall(page)
await page.click('li:has-text("My folder #1")')
await expect(page.locator('h1:has-text("My folder #1")')).toBeVisible()
await createFolderButton.click()
await page.click('text="New folder"')
await page.fill('input', 'My folder #2')
await page.press('input', 'Enter')
await waitForNextApiCall(page)
await page.click('li:has-text("My folder #2")')
await expect(page.locator('h1 >> text="My folder #2"')).toBeVisible()
await page.click('text="Back"')
await expect(page.locator('span >> text="My folder #2"')).toBeVisible()
await page.click('text="Back"')
await expect(page.locator('span >> text=My folder #1')).toBeVisible()
})
test('folders and typebots should be deletable', async ({ page }) => {
await createFolders([{ name: 'Folder #1' }, { name: 'Folder #2' }])
await createTypebots([{ id: 'deletable-typebot', name: 'Typebot #1' }])
await page.goto('/typebots')
await page.click('button[aria-label="Show Folder #1 menu"]')
await page.click('li:has-text("Folder #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Folder #1"')).not.toBeVisible()
await page.click('button[aria-label="Show Typebot #1 menu"]')
await page.click('li:has-text("Typebot #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Typebot #1"')).not.toBeVisible()
})
test('folders and typebots should be movable', async ({ page }) => {
const droppableFolderId = cuid()
await createFolders([{ id: droppableFolderId, name: 'Droppable folder' }])
await createTypebots([{ name: 'Draggable typebot' }])
await page.goto('/typebots')
const typebotButton = page.locator('li:has-text("Draggable typebot")')
const folderButton = page.locator('li:has-text("Droppable folder")')
await page.dragAndDrop(
'li:has-text("Draggable typebot")',
'li:has-text("Droppable folder")'
)
await waitForNextApiCall(page)
await expect(typebotButton).toBeHidden()
await folderButton.click()
await expect(page).toHaveURL(new RegExp(`/folders/${droppableFolderId}`))
await expect(typebotButton).toBeVisible()
await page.dragAndDrop(
'li:has-text("Draggable typebot")',
'a:has-text("Back")'
)
await waitForNextApiCall(page)
await expect(typebotButton).toBeHidden()
await page.click('a:has-text("Back")')
await expect(typebotButton).toBeVisible()
})
test.describe('Free user', () => {
test("create folder shouldn't be available", async ({ page }) => {
await page.goto('/typebots') await page.goto('/typebots')
const createFolderButton = page.locator( await page.click('text="Pro workspace"')
'button:has-text("Create a folder")' await page.click('text="Free workspace"')
) await expect(page.locator('[data-testid="starter-plan-tag"]')).toBeVisible()
await expect(createFolderButton).not.toBeDisabled() await page.click('text=Create a folder')
await createFolderButton.click() await expect(
await page.click('text="New folder"') page.locator(
await page.fill('input[value="New folder"]', 'My folder #1') 'text="You need to upgrade your plan in order to create folders"'
await page.press('input[value="My folder #1"]', 'Enter') )
await waitForNextApiCall(page) ).toBeVisible()
await page.click('li:has-text("My folder #1")')
await expect(page.locator('h1:has-text("My folder #1")')).toBeVisible()
await createFolderButton.click()
await page.click('text="New folder"')
await page.fill('input', 'My folder #2')
await page.press('input', 'Enter')
await waitForNextApiCall(page)
await page.click('li:has-text("My folder #2")')
await expect(page.locator('h1 >> text="My folder #2"')).toBeVisible()
await page.click('text="Back"')
await expect(page.locator('span >> text="My folder #2"')).toBeVisible()
await page.click('text="Back"')
await expect(page.locator('span >> text=My folder #1')).toBeVisible()
})
test('folders and typebots should be deletable', async ({ page }) => {
await createFolders([{ name: 'Folder #1' }, { name: 'Folder #2' }])
await createTypebots([{ id: 'deletable-typebot', name: 'Typebot #1' }])
await page.goto('/typebots')
await page.click('button[aria-label="Show Folder #1 menu"]')
await page.click('li:has-text("Folder #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Folder #1"')).not.toBeVisible()
await page.click('button[aria-label="Show Typebot #1 menu"]')
await page.click('li:has-text("Typebot #1") >> button:has-text("Delete")')
await deleteButtonInConfirmDialog(page).click()
await expect(page.locator('span >> text="Typebot #1"')).not.toBeVisible()
})
test('folders and typebots should be movable', async ({ page }) => {
const droppableFolderId = cuid()
await createFolders([{ id: droppableFolderId, name: 'Droppable folder' }])
await createTypebots([{ name: 'Draggable typebot' }])
await page.goto('/typebots')
const typebotButton = page.locator('li:has-text("Draggable typebot")')
const folderButton = page.locator('li:has-text("Droppable folder")')
await page.dragAndDrop(
'li:has-text("Draggable typebot")',
'li:has-text("Droppable folder")'
)
await waitForNextApiCall(page)
await expect(typebotButton).toBeHidden()
await folderButton.click()
await expect(page).toHaveURL(new RegExp(`/folders/${droppableFolderId}`))
await expect(typebotButton).toBeVisible()
await page.dragAndDrop(
'li:has-text("Draggable typebot")',
'a:has-text("Back")'
)
await waitForNextApiCall(page)
await expect(typebotButton).toBeHidden()
await page.click('a:has-text("Back")')
await expect(typebotButton).toBeVisible()
})
test.describe('Free user', () => {
test.use({
storageState: path.join(__dirname, '../secondUser.json'),
})
test("create folder shouldn't be available", async ({ page }) => {
await page.goto('/typebots')
await page.click('text=Create a folder')
await expect(page.locator('text=For solo creator')).toBeVisible()
})
}) })
}) })

View File

@@ -9,174 +9,174 @@ import path from 'path'
import cuid from 'cuid' import cuid from 'cuid'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
test.describe.parallel('Editor', () => { test.describe.configure({ mode: 'parallel' })
test('Edges connection should work', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator("text='Start'")).toBeVisible()
await page.dragAndDrop('text=Button', '#editor-container', {
targetPosition: { x: 1000, y: 400 },
})
await page.dragAndDrop(
'text=Text >> nth=0',
'[data-testid="group"] >> nth=1',
{
targetPosition: { x: 100, y: 50 },
}
)
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="group"] >> nth=1',
{ targetPosition: { x: 100, y: 10 } }
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="group"] >> nth=1'
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop('text=Date', '#editor-container', {
targetPosition: { x: 1000, y: 800 },
})
await page.dragAndDrop(
'[data-testid="endpoint"] >> nth=2',
'[data-testid="group"] >> nth=2',
{
targetPosition: { x: 100, y: 10 },
}
)
await expect(page.locator('[data-testid="edge"] >> nth=0')).toBeVisible()
await expect(page.locator('[data-testid="edge"] >> nth=1')).toBeVisible()
await page.click('[data-testid="clickable-edge"] >> nth=0', { test('Edges connection should work', async ({ page }) => {
force: true, const typebotId = cuid()
button: 'right', await createTypebots([
}) {
await page.click('text=Delete') id: typebotId,
const total = await page.locator('[data-testid="edge"]').count() },
expect(total).toBe(1) ])
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator("text='Start'")).toBeVisible()
await page.dragAndDrop('text=Button', '#editor-container', {
targetPosition: { x: 1000, y: 400 },
}) })
test('Drag and drop blocks and items should work', async ({ page }) => { await page.dragAndDrop(
const typebotId = cuid() 'text=Text >> nth=0',
await importTypebotInDatabase( '[data-testid="group"] >> nth=1',
path.join(__dirname, '../fixtures/typebots/editor/buttonsDnd.json'), {
{ targetPosition: { x: 100, y: 50 },
id: typebotId, }
} )
) await page.dragAndDrop(
'[data-testid="endpoint"]',
// Blocks dnd '[data-testid="group"] >> nth=1',
await page.goto(`/typebots/${typebotId}/edit`) { targetPosition: { x: 100, y: 10 } }
await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText( )
'Hello!' await expect(page.locator('[data-testid="edge"]')).toBeVisible()
) await page.dragAndDrop(
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=3', { '[data-testid="endpoint"]',
targetPosition: { x: 100, y: 0 }, '[data-testid="group"] >> nth=1'
}) )
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText( await expect(page.locator('[data-testid="edge"]')).toBeVisible()
'Hello!' await page.dragAndDrop('text=Date', '#editor-container', {
) targetPosition: { x: 1000, y: 800 },
await page.dragAndDrop('text=Hello', 'text=Group #2')
await expect(page.locator('[data-testid="block"] >> nth=3')).toHaveText(
'Hello!'
)
// Items dnd
await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText(
'Item 1'
)
await page.dragAndDrop('text=Item 1', 'text=Item 3')
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
'Item 1'
)
await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText(
'Item 3'
)
await page.dragAndDrop('text=Item 3', 'text=Item 2-3')
await expect(page.locator('[data-testid="item"] >> nth=6')).toHaveText(
'Item 3'
)
}) })
test('Undo / Redo buttons should work', async ({ page }) => { await page.dragAndDrop(
const typebotId = cuid() '[data-testid="endpoint"] >> nth=2',
await createTypebots([ '[data-testid="group"] >> nth=2',
{ {
id: typebotId, targetPosition: { x: 100, y: 10 },
...parseDefaultGroupWithBlock({ }
type: InputBlockType.TEXT, )
options: defaultTextInputOptions, await expect(page.locator('[data-testid="edge"] >> nth=0')).toBeVisible()
}), await expect(page.locator('[data-testid="edge"] >> nth=1')).toBeVisible()
},
])
await page.goto(`/typebots/${typebotId}/edit`) await page.click('[data-testid="clickable-edge"] >> nth=0', {
await page.click('text=Group #1', { button: 'right' }) force: true,
await page.click('text=Duplicate') button: 'right',
await expect(page.locator('text="Group #1"')).toBeVisible()
await expect(page.locator('text="Group #1 copy"')).toBeVisible()
await page.click('text="Group #1"', { button: 'right' })
await page.click('text=Delete')
await expect(page.locator('text="Group #1"')).toBeHidden()
await page.click('button[aria-label="Undo"]')
await expect(page.locator('text="Group #1"')).toBeVisible()
await page.click('button[aria-label="Redo"]')
await expect(page.locator('text="Group #1"')).toBeHidden()
})
test('Rename and icon change should work', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
name: 'My awesome typebot',
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[data-testid="editable-icon"]')
await expect(page.locator('text="My awesome typebot"')).toBeVisible()
await page.fill('input[placeholder="Search..."]', 'love')
await page.click('text="😍"')
await page.click('text="My awesome typebot"')
await page.fill('input[value="My awesome typebot"]', 'My superb typebot')
await page.press('input[value="My superb typebot"]', 'Enter')
await page.click('[aria-label="Navigate back"]')
await expect(page.locator('text="😍"')).toBeVisible()
await expect(page.locator('text="My superb typebot"')).toBeVisible()
})
test('Preview from group should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/editor/previewFromGroup.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[aria-label="Preview bot from this group"] >> nth=1')
await expect(
typebotViewer(page).locator('text="Hello this is group 1"')
).toBeVisible()
await page.click('[aria-label="Preview bot from this group"] >> nth=2')
await expect(
typebotViewer(page).locator('text="Hello this is group 2"')
).toBeVisible()
await page.click('[aria-label="Close"]')
await page.click('text="Preview"')
await expect(
typebotViewer(page).locator('text="Hello this is group 1"')
).toBeVisible()
}) })
await page.click('text=Delete')
const total = await page.locator('[data-testid="edge"]').count()
expect(total).toBe(1)
})
test('Drag and drop blocks and items should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/editor/buttonsDnd.json'),
{
id: typebotId,
}
)
// Blocks dnd
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=3', {
targetPosition: { x: 100, y: 0 },
})
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', 'text=Group #2')
await expect(page.locator('[data-testid="block"] >> nth=3')).toHaveText(
'Hello!'
)
// Items dnd
await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText(
'Item 1'
)
await page.dragAndDrop('text=Item 1', 'text=Item 3')
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
'Item 1'
)
await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText(
'Item 3'
)
await page.dragAndDrop('text=Item 3', 'text=Item 2-3')
await expect(page.locator('[data-testid="item"] >> nth=6')).toHaveText(
'Item 3'
)
})
test('Undo / Redo buttons should work', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Group #1', { button: 'right' })
await page.click('text=Duplicate')
await expect(page.locator('text="Group #1"')).toBeVisible()
await expect(page.locator('text="Group #1 copy"')).toBeVisible()
await page.click('text="Group #1"', { button: 'right' })
await page.click('text=Delete')
await expect(page.locator('text="Group #1"')).toBeHidden()
await page.click('button[aria-label="Undo"]')
await expect(page.locator('text="Group #1"')).toBeVisible()
await page.click('button[aria-label="Redo"]')
await expect(page.locator('text="Group #1"')).toBeHidden()
})
test('Rename and icon change should work', async ({ page }) => {
const typebotId = cuid()
await createTypebots([
{
id: typebotId,
name: 'My awesome typebot',
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[data-testid="editable-icon"]')
await expect(page.locator('text="My awesome typebot"')).toBeVisible()
await page.fill('input[placeholder="Search..."]', 'love')
await page.click('text="😍"')
await page.click('text="My awesome typebot"')
await page.fill('input[value="My awesome typebot"]', 'My superb typebot')
await page.press('input[value="My superb typebot"]', 'Enter')
await page.click('[aria-label="Navigate back"]')
await expect(page.locator('text="😍"')).toBeVisible()
await expect(page.locator('text="My superb typebot"')).toBeVisible()
})
test('Preview from group should work', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/editor/previewFromGroup.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[aria-label="Preview bot from this group"] >> nth=1')
await expect(
typebotViewer(page).locator('text="Hello this is group 1"')
).toBeVisible()
await page.click('[aria-label="Preview bot from this group"] >> nth=2')
await expect(
typebotViewer(page).locator('text="Hello this is group 2"')
).toBeVisible()
await page.click('[aria-label="Close"]')
await page.click('text="Preview"')
await expect(
typebotViewer(page).locator('text="Hello this is group 1"')
).toBeVisible()
}) })

View File

@@ -62,7 +62,7 @@ test.describe('Payment input block', () => {
await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240') await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240')
await typebotViewer(page).locator(`text="Pay 30€"`).click() await typebotViewer(page).locator(`text="Pay 30€"`).click()
await expect( await expect(
typebotViewer(page).locator(`text="Your card was declined."`) typebotViewer(page).locator(`text="Your card has been declined."`)
).toBeVisible() ).toBeVisible()
await stripePaymentForm(page) await stripePaymentForm(page)
.locator(`[placeholder="1234 1234 1234 1234"]`) .locator(`[placeholder="1234 1234 1234 1234"]`)

View File

@@ -43,7 +43,7 @@ test('results should be deletable', async ({ page }) => {
}), }),
}, },
]) ])
await createResults({ typebotId, count: 200 }) await createResults({ typebotId, count: 200, isChronological: true })
await page.goto(`/typebots/${typebotId}/results`) await page.goto(`/typebots/${typebotId}/results`)
await selectFirstResults(page) await selectFirstResults(page)
await page.click('text="Delete"') await page.click('text="Delete"')
@@ -67,7 +67,7 @@ test('submissions table should have infinite scroll', async ({ page }) => {
tableWrapper.scrollTo(0, tableWrapper.scrollHeight) tableWrapper.scrollTo(0, tableWrapper.scrollHeight)
}) })
await createResults({ typebotId, count: 200 }) await createResults({ typebotId, count: 200, isChronological: true })
await page.goto(`/typebots/${typebotId}/results`) await page.goto(`/typebots/${typebotId}/results`)
await expect(page.locator('text=content199')).toBeVisible() await expect(page.locator('text=content199')).toBeVisible()

View File

@@ -135,9 +135,13 @@ test.describe.parallel('Settings page', () => {
typebotViewer(page).locator('text="What\'s your name?"') typebotViewer(page).locator('text="What\'s your name?"')
).toBeVisible() ).toBeVisible()
await page.click('button:has-text("General")') await page.click('button:has-text("General")')
await expect(page.locator('text=Pro')).toBeVisible() await expect(page.locator('text=Starter')).toBeVisible()
await page.click('text=Typebot.io branding') await page.click('text=Typebot.io branding')
await expect(page.locator('text=For solo creator')).toBeVisible() await expect(
page.locator(
'text="You need to upgrade your plan in order to remove branding"'
)
).toBeVisible()
}) })
}) })
}) })

View File

@@ -1,7 +1,7 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import cuid from 'cuid' import cuid from 'cuid'
import { defaultTextInputOptions, InputBlockType } from 'models' import { defaultTextInputOptions, InputBlockType } from 'models'
import { connectedAsOtherUser } from 'playwright/services/browser' import { mockSessionResponsesToOtherUser } from 'playwright/services/browser'
import { import {
createTypebots, createTypebots,
parseDefaultGroupWithBlock, parseDefaultGroupWithBlock,
@@ -129,7 +129,7 @@ test('can manage members', async ({ page }) => {
await page.click('button >> text="Remove"') await page.click('button >> text="Remove"')
await expect(page.locator('text="guest@email.com"')).toBeHidden() await expect(page.locator('text="guest@email.com"')).toBeHidden()
await connectedAsOtherUser(page) await mockSessionResponsesToOtherUser(page)
await page.goto('/typebots') await page.goto('/typebots')
await page.click('text=Settings & Members') await page.click('text=Settings & Members')
await expect(page.locator('text="Settings"')).toBeHidden() await expect(page.locator('text="Settings"')).toBeHidden()

View File

@@ -17,8 +17,8 @@ export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined, dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
}) })
return { return {
members: data?.members, members: data?.members ?? [],
invitations: data?.invitations, invitations: data?.invitations ?? [],
isLoading: !error && !data, isLoading: !error && !data,
mutate, mutate,
} }

View File

@@ -1,7 +1,7 @@
import { WorkspaceWithMembers } from 'contexts/WorkspaceContext' import { WorkspaceWithMembers } from 'contexts/WorkspaceContext'
import { Plan, Workspace } from 'db' import { Plan, Workspace } from 'db'
import useSWR from 'swr' import useSWR from 'swr'
import { isNotDefined, sendRequest } from 'utils' import { isDefined, isNotDefined, sendRequest } from 'utils'
import { fetcher } from '../utils' import { fetcher } from '../utils'
export const useWorkspaces = ({ userId }: { userId?: string }) => { export const useWorkspaces = ({ userId }: { userId?: string }) => {
@@ -74,3 +74,6 @@ export const planToReadable = (plan?: Plan) => {
export const isFreePlan = (workspace?: Pick<Workspace, 'plan'>) => export const isFreePlan = (workspace?: Pick<Workspace, 'plan'>) =>
isNotDefined(workspace) || workspace?.plan === Plan.FREE isNotDefined(workspace) || workspace?.plan === Plan.FREE
export const isWorkspaceProPlan = (workspace?: Pick<Workspace, 'plan'>) =>
isDefined(workspace) && workspace.plan === Plan.PRO

View File

@@ -17,16 +17,16 @@
"dependencies": { "dependencies": {
"@docusaurus/core": "2.1.0", "@docusaurus/core": "2.1.0",
"@docusaurus/preset-classic": "2.1.0", "@docusaurus/preset-classic": "2.1.0",
"@docusaurus/theme-search-algolia": "^2.1.0", "@docusaurus/theme-search-algolia": "2.1.0",
"@docusaurus/theme-common": "2.1.0", "@docusaurus/theme-common": "2.1.0",
"react": "^17.0.2", "react": "17.0.2",
"react-dom": "^17.0.2", "react-dom": "17.0.2",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "1.6.22",
"@svgr/webpack": "^6.3.1", "@svgr/webpack": "6.3.1",
"clsx": "^1.2.1", "clsx": "1.2.1",
"file-loader": "^6.2.0", "file-loader": "6.2.0",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "1.3.5",
"url-loader": "^4.1.1" "url-loader": "4.1.1"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -41,9 +41,9 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@algolia/client-search": "^4.14.2", "@algolia/client-search": "4.14.2",
"@types/react": "^18.0.19", "@types/react": "18.0.19",
"typescript": "^4.8.3", "typescript": "4.8.3",
"webpack": "^5.74.0" "webpack": "5.74.0"
} }
} }

View File

@@ -23,14 +23,12 @@ interface PricingCardProps extends CardProps {
data: PricingCardData data: PricingCardData
icon?: JSX.Element icon?: JSX.Element
button: React.ReactElement button: React.ReactElement
isMostPopular?: boolean
} }
export const PricingCard = ({ export const PricingCard = ({
data, data,
icon, icon,
button, button,
isMostPopular,
...rest ...rest
}: PricingCardProps) => { }: PricingCardProps) => {
const { features, price, name } = data const { features, price, name } = data

View File

@@ -219,7 +219,6 @@ const Pricing = () => {
<ActionButton>Subscribe now</ActionButton> <ActionButton>Subscribe now</ActionButton>
</NextChakraLink> </NextChakraLink>
} }
isMostPopular
/> />
</Stack> </Stack>
<VStack maxW="1200px" w="full" spacing={[12, 20]} px="4"> <VStack maxW="1200px" w="full" spacing={[12, 20]} px="4">

View File

@@ -76,6 +76,7 @@ const checkStorageLimit = async (typebotId: string) => {
}) })
if (!typebot?.workspace) throw new Error('Workspace not found') if (!typebot?.workspace) throw new Error('Workspace not found')
const { workspace } = typebot const { workspace } = typebot
console.log(typebot.workspaceId)
const { const {
_sum: { storageUsed: totalStorageUsed }, _sum: { storageUsed: totalStorageUsed },
} = await prisma.answer.aggregate({ } = await prisma.answer.aggregate({
@@ -94,26 +95,27 @@ const checkStorageLimit = async (typebotId: string) => {
if (!totalStorageUsed) return false if (!totalStorageUsed) return false
const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null
const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null
const storageLimit = getStorageLimit(typebot.workspace) * 1024 * 1024 * 1024 const storageLimit = getStorageLimit(typebot.workspace)
const storageLimitBytes = storageLimit * 1024 * 1024 * 1024
if ( if (
totalStorageUsed >= storageLimit * LIMIT_EMAIL_TRIGGER_PERCENT && totalStorageUsed >= storageLimitBytes * LIMIT_EMAIL_TRIGGER_PERCENT &&
!hasSentFirstEmail && !hasSentFirstEmail &&
env('E2E_TEST') !== 'true' env('E2E_TEST') !== 'true'
) )
sendAlmostReachStorageLimitEmail({ await sendAlmostReachStorageLimitEmail({
workspaceId: workspace.id, workspaceId: workspace.id,
storageLimit, storageLimit,
}) })
if ( if (
totalStorageUsed >= storageLimit && totalStorageUsed >= storageLimitBytes &&
!hasSentSecondEmail && !hasSentSecondEmail &&
env('E2E_TEST') !== 'true' env('E2E_TEST') !== 'true'
) )
sendReachStorageLimitEmail({ await sendReachStorageLimitEmail({
workspaceId: workspace.id, workspaceId: workspace.id,
storageLimit, storageLimit,
}) })
return (totalStorageUsed ?? 0) >= getStorageLimit(typebot?.workspace) return totalStorageUsed >= storageLimitBytes
} }
const sendAlmostReachStorageLimitEmail = async ({ const sendAlmostReachStorageLimitEmail = async ({

View File

@@ -69,8 +69,8 @@ const checkChatsUsage = async (
| 'chatsLimitSecondEmailSentAt' | 'chatsLimitSecondEmailSentAt'
> >
) => { ) => {
const chatLimit = getChatsLimit(workspace) const chatsLimit = getChatsLimit(workspace)
if (chatLimit === -1) return if (chatsLimit === -1) return
const now = new Date() const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0) const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
@@ -91,26 +91,26 @@ const checkChatsUsage = async (
workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth && workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth &&
workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth
if ( if (
chatsCount >= chatLimit * LIMIT_EMAIL_TRIGGER_PERCENT && chatsCount >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
!hasSentFirstEmail && !hasSentFirstEmail &&
env('E2E_TEST') !== 'true' env('E2E_TEST') !== 'true'
) )
await sendAlmostReachChatsLimitEmail({ await sendAlmostReachChatsLimitEmail({
workspaceId: workspace.id, workspaceId: workspace.id,
chatLimit, chatLimit: chatsLimit,
firstDayOfNextMonth, firstDayOfNextMonth,
}) })
if ( if (
chatsCount >= chatLimit && chatsCount >= chatsLimit &&
!hasSentSecondEmail && !hasSentSecondEmail &&
env('E2E_TEST') !== 'true' env('E2E_TEST') !== 'true'
) )
await sendReachedAlertEmail({ await sendReachedAlertEmail({
workspaceId: workspace.id, workspaceId: workspace.id,
chatLimit, chatLimit: chatsLimit,
firstDayOfNextMonth, firstDayOfNextMonth,
}) })
return chatsCount >= chatLimit return chatsCount >= chatsLimit
} }
const sendAlmostReachChatsLimitEmail = async ({ const sendAlmostReachChatsLimitEmail = async ({

View File

@@ -1,11 +1,11 @@
import { Page } from '@playwright/test' import { Page } from '@playwright/test'
export const mockSessionApiCalls = (page: Page) => export const mockSessionResponsesToOtherUser = async (page: Page) =>
page.route(`${process.env.BUILDER_URL}/api/auth/session`, (route) => { page.route('/api/auth/session', (route) => {
if (route.request().method() === 'GET') { if (route.request().method() === 'GET') {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
body: '{"user":{"id":"proUser","name":"Pro user","email":"pro-user@email.com","emailVerified":null,"image":"https://avatars.githubusercontent.com/u/16015833?v=4","stripeId":null,"graphNavigation": "TRACKPAD"}}', body: '{"user":{"id":"otherUserId","name":"James Doe","email":"other-user@email.com","emailVerified":null,"image":"https://avatars.githubusercontent.com/u/16015833?v=4","stripeId":null,"graphNavigation": "TRACKPAD"}}',
}) })
} }
return route.continue() return route.continue()

View File

@@ -18,6 +18,8 @@ const prisma = new PrismaClient()
const userId = 'userId' const userId = 'userId'
export const freeWorkspaceId = 'freeWorkspace' export const freeWorkspaceId = 'freeWorkspace'
export const starterWorkspaceId = 'starterWorkspace' export const starterWorkspaceId = 'starterWorkspace'
export const limitTestWorkspaceId = 'limitTestWorkspace'
export const apiToken = 'jirowjgrwGREHE'
export const teardownDatabase = async () => { export const teardownDatabase = async () => {
await prisma.workspace.deleteMany({ await prisma.workspace.deleteMany({
@@ -51,6 +53,11 @@ export const createWorkspaces = async () =>
name: 'Starter workspace', name: 'Starter workspace',
plan: Plan.STARTER, plan: Plan.STARTER,
}, },
{
id: limitTestWorkspaceId,
name: 'Limit test workspace',
plan: Plan.FREE,
},
], ],
}) })
@@ -65,20 +72,10 @@ export const createUser = async () => {
createMany: { createMany: {
data: [ data: [
{ {
name: 'Token 1', name: 'Token',
token: 'jirowjgrwGREHEtoken1', token: apiToken,
createdAt: new Date(2022, 1, 1), createdAt: new Date(2022, 1, 1),
}, },
{
name: 'Github',
token: 'jirowjgrwGREHEgdrgithub',
createdAt: new Date(2022, 1, 2),
},
{
name: 'N8n',
token: 'jirowjgrwGREHrgwhrwn8n',
createdAt: new Date(2022, 1, 3),
},
], ],
}, },
}, },
@@ -88,6 +85,7 @@ export const createUser = async () => {
data: [ data: [
{ role: WorkspaceRole.ADMIN, userId, workspaceId: freeWorkspaceId }, { role: WorkspaceRole.ADMIN, userId, workspaceId: freeWorkspaceId },
{ role: WorkspaceRole.ADMIN, userId, workspaceId: starterWorkspaceId }, { role: WorkspaceRole.ADMIN, userId, workspaceId: starterWorkspaceId },
{ role: WorkspaceRole.ADMIN, userId, workspaceId: limitTestWorkspaceId },
], ],
}) })
} }
@@ -207,8 +205,8 @@ export const importTypebotInDatabase = async (
) => { ) => {
const typebot: Typebot = { const typebot: Typebot = {
...JSON.parse(readFileSync(path).toString()), ...JSON.parse(readFileSync(path).toString()),
...updates,
workspaceId: starterWorkspaceId, workspaceId: starterWorkspaceId,
...updates,
} }
await prisma.typebot.create({ await prisma.typebot.create({
data: typebot, data: typebot,

View File

@@ -1,5 +1,6 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { import {
apiToken,
createResults, createResults,
createWebhook, createWebhook,
importTypebotInDatabase, importTypebotInDatabase,
@@ -23,7 +24,7 @@ test.beforeAll(async () => {
test('can list typebots', async ({ request }) => { test('can list typebots', async ({ request }) => {
expect((await request.get(`/api/typebots`)).status()).toBe(401) expect((await request.get(`/api/typebots`)).status()).toBe(401)
const response = await request.get(`/api/typebots`, { const response = await request.get(`/api/typebots`, {
headers: { Authorization: 'Bearer userToken' }, headers: { Authorization: `Bearer ${apiToken}` },
}) })
const { typebots } = await response.json() const { typebots } = await response.json()
expect(typebots).toHaveLength(1) expect(typebots).toHaveLength(1)
@@ -41,7 +42,7 @@ test('can get webhook blocks', async ({ request }) => {
const response = await request.get( const response = await request.get(
`/api/typebots/${typebotId}/webhookBlocks`, `/api/typebots/${typebotId}/webhookBlocks`,
{ {
headers: { Authorization: 'Bearer userToken' }, headers: { Authorization: `Bearer ${apiToken}` },
} }
) )
const { blocks } = await response.json() const { blocks } = await response.json()
@@ -65,7 +66,7 @@ test('can subscribe webhook', async ({ request }) => {
`/api/typebots/${typebotId}/blocks/webhookBlock/subscribeWebhook`, `/api/typebots/${typebotId}/blocks/webhookBlock/subscribeWebhook`,
{ {
headers: { headers: {
Authorization: 'Bearer userToken', Authorization: `Bearer ${apiToken}`,
}, },
data: { url: 'https://test.com' }, data: { url: 'https://test.com' },
} }
@@ -87,7 +88,7 @@ test('can unsubscribe webhook', async ({ request }) => {
const response = await request.post( const response = await request.post(
`/api/typebots/${typebotId}/blocks/webhookBlock/unsubscribeWebhook`, `/api/typebots/${typebotId}/blocks/webhookBlock/unsubscribeWebhook`,
{ {
headers: { Authorization: 'Bearer userToken' }, headers: { Authorization: `Bearer ${apiToken}` },
} }
) )
const body = await response.json() const body = await response.json()
@@ -107,7 +108,7 @@ test('can get a sample result', async ({ request }) => {
const response = await request.get( const response = await request.get(
`/api/typebots/${typebotId}/blocks/webhookBlock/sampleResult`, `/api/typebots/${typebotId}/blocks/webhookBlock/sampleResult`,
{ {
headers: { Authorization: 'Bearer userToken' }, headers: { Authorization: `Bearer ${apiToken}` },
} }
) )
const data = await response.json() const data = await response.json()
@@ -128,7 +129,7 @@ test('can list results', async ({ request }) => {
const response = await request.get( const response = await request.get(
`/api/typebots/${typebotId}/results?limit=10`, `/api/typebots/${typebotId}/results?limit=10`,
{ {
headers: { Authorization: 'Bearer userToken' }, headers: { Authorization: `Bearer ${apiToken}` },
} }
) )
const { results } = await response.json() const { results } = await response.json()

View File

@@ -6,7 +6,6 @@ import { typebotViewer } from '../services/selectorUtils'
import { createResults, importTypebotInDatabase } from '../services/database' import { createResults, importTypebotInDatabase } from '../services/database'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { isDefined } from 'utils' import { isDefined } from 'utils'
import { describe } from 'node:test'
const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024 const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024
@@ -49,7 +48,7 @@ test('should work as expected', async ({ page, browser }) => {
page.locator('text="Export"').click(), page.locator('text="Export"').click(),
]) ])
const downloadPath = await download.path() const downloadPath = await download.path()
expect(path).toBeDefined() expect(downloadPath).toBeDefined()
const file = readFileSync(downloadPath as string).toString() const file = readFileSync(downloadPath as string).toString()
const { data } = parse(file) const { data } = parse(file)
expect(data).toHaveLength(2) expect(data).toHaveLength(2)
@@ -86,7 +85,7 @@ test('should work as expected', async ({ page, browser }) => {
).toBeVisible() ).toBeVisible()
}) })
describe('Storage limit is reached', () => { test.describe('Storage limit is reached', () => {
const typebotId = cuid() const typebotId = cuid()
test.beforeAll(async () => { test.beforeAll(async () => {

View File

@@ -3,9 +3,6 @@ import path from 'path'
import { importTypebotInDatabase } from '../services/database' import { importTypebotInDatabase } from '../services/database'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('should work as expected', async ({ page }) => { test('should work as expected', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { import {
createResults, createResults,
freeWorkspaceId,
importTypebotInDatabase, importTypebotInDatabase,
limitTestWorkspaceId,
} from '../services/database' } from '../services/database'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
@@ -14,7 +14,7 @@ test('should not start if chat limit is reached', async ({ page }) => {
{ {
id: typebotId, id: typebotId,
publicId: `${typebotId}-public`, publicId: `${typebotId}-public`,
workspaceId: freeWorkspaceId, workspaceId: limitTestWorkspaceId,
} }
) )
await createResults({ typebotId, count: 320 }) await createResults({ typebotId, count: 320 })

View File

@@ -11,9 +11,6 @@ import {
} from 'models' } from 'models'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import cuid from 'cuid' import cuid from 'cuid'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('Should correctly parse metadata', async ({ page }) => { test('Should correctly parse metadata', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()
@@ -37,20 +34,20 @@ test('Should correctly parse metadata', async ({ page }) => {
}, },
]) ])
await page.goto(`/${typebotId}-public`) await page.goto(`/${typebotId}-public`)
await expect( expect(
await page.evaluate(`document.querySelector('title').textContent`) await page.evaluate(`document.querySelector('title').textContent`)
).toBe(customMetadata.title) ).toBe(customMetadata.title)
await expect( expect(
await page.evaluate( await page.evaluate(
() => (document.querySelector('meta[name="description"]') as any).content () => (document.querySelector('meta[name="description"]') as any).content
) )
).toBe(customMetadata.description) ).toBe(customMetadata.description)
await expect( expect(
await page.evaluate( await page.evaluate(
() => (document.querySelector('meta[property="og:image"]') as any).content () => (document.querySelector('meta[property="og:image"]') as any).content
) )
).toBe(customMetadata.imageUrl) ).toBe(customMetadata.imageUrl)
await expect( expect(
await page.evaluate(() => await page.evaluate(() =>
(document.querySelector('link[rel="icon"]') as any).getAttribute('href') (document.querySelector('link[rel="icon"]') as any).getAttribute('href')
) )

View File

@@ -3,9 +3,6 @@ import { importTypebotInDatabase } from '../services/database'
import cuid from 'cuid' import cuid from 'cuid'
import path from 'path' import path from 'path'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('should correctly be injected', async ({ page }) => { test('should correctly be injected', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@@ -7,9 +7,6 @@ import cuid from 'cuid'
import path from 'path' import path from 'path'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import { SmtpCredentialsData } from 'models' import { SmtpCredentialsData } from 'models'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
const mockSmtpCredentials: SmtpCredentialsData = { const mockSmtpCredentials: SmtpCredentialsData = {
from: { from: {

View File

@@ -10,9 +10,6 @@ import {
defaultTextInputOptions, defaultTextInputOptions,
InputBlockType, InputBlockType,
} from 'models' } from 'models'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('Result should be in storage by default', async ({ page }) => { test('Result should be in storage by default', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@@ -2,9 +2,6 @@ import test, { expect } from '@playwright/test'
import path from 'path' import path from 'path'
import { importTypebotInDatabase } from '../services/database' import { importTypebotInDatabase } from '../services/database'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('should work as expected', async ({ page }) => { test('should work as expected', async ({ page }) => {
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm' const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'

View File

@@ -4,9 +4,6 @@ import cuid from 'cuid'
import path from 'path' import path from 'path'
import { typebotViewer } from '../services/selectorUtils' import { typebotViewer } from '../services/selectorUtils'
import { HttpMethod } from 'models' import { HttpMethod } from 'models'
import { mockSessionApiCalls } from 'playwright/services/browser'
test.beforeEach(({ page }) => mockSessionApiCalls(page))
test('should execute webhooks properly', async ({ page }) => { test('should execute webhooks properly', async ({ page }) => {
const typebotId = cuid() const typebotId = cuid()

View File

@@ -80,6 +80,7 @@ export const ConversationContainer = ({
} }
const nextGroup = currentTypebot.groups.find(byId(nextEdge.to.groupId)) const nextGroup = currentTypebot.groups.find(byId(nextEdge.to.groupId))
if (!nextGroup) return onCompleted() if (!nextGroup) return onCompleted()
console.log(nextGroup, nextEdge)
const startBlockIndex = nextEdge.to.blockId const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId)) ? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0 : 0
@@ -151,9 +152,7 @@ export const ConversationContainer = ({
const groupAfter = displayedGroups[idx + 1] const groupAfter = displayedGroups[idx + 1]
const groupAfterStartsWithInput = const groupAfterStartsWithInput =
groupAfter && groupAfter &&
isInputBlock( isInputBlock(groupAfter.group.blocks[groupAfter.startBlockIndex])
groupAfter.group.blocks[groupAfter.startBlockIndex] as Block
)
return ( return (
<ChatGroup <ChatGroup
key={displayedGroup.group.id + idx} key={displayedGroup.group.id + idx}

View File

@@ -8,6 +8,7 @@
"devDependencies": { "devDependencies": {
"@types/nodemailer": "6.4.5", "@types/nodemailer": "6.4.5",
"aws-sdk": "2.1213.0", "aws-sdk": "2.1213.0",
"cuid": "^2.1.8",
"db": "workspace:*", "db": "workspace:*",
"models": "workspace:*", "models": "workspace:*",
"next": "12.3.0", "next": "12.3.0",

View File

@@ -1,9 +1,10 @@
import { PrismaClient } from 'db' import { PrismaClient } from 'db'
import cuid from 'cuid'
type CreateFakeResultsProps = { type CreateFakeResultsProps = {
typebotId: string typebotId: string
count: number count: number
idPrefix?: string customResultIdPrefix?: string
isChronological?: boolean isChronological?: boolean
fakeStorage?: number fakeStorage?: number
} }
@@ -12,18 +13,19 @@ export const injectFakeResults =
(prisma: PrismaClient) => (prisma: PrismaClient) =>
async ({ async ({
count, count,
idPrefix = '', customResultIdPrefix,
typebotId, typebotId,
isChronological = true, isChronological,
fakeStorage, fakeStorage,
}: CreateFakeResultsProps) => { }: CreateFakeResultsProps) => {
const resultIdPrefix = customResultIdPrefix ?? cuid()
await prisma.result.createMany({ await prisma.result.createMany({
data: [ data: [
...Array.from(Array(count)).map((_, idx) => { ...Array.from(Array(count)).map((_, idx) => {
const today = new Date() const today = new Date()
const rand = Math.random() const rand = Math.random()
return { return {
id: `${idPrefix}-result${idx}`, id: `${resultIdPrefix}-result${idx}`,
typebotId, typebotId,
createdAt: isChronological createdAt: isChronological
? new Date( ? new Date(
@@ -36,20 +38,23 @@ export const injectFakeResults =
}), }),
], ],
}) })
return createAnswers(prisma)({ idPrefix, fakeStorage, count }) return createAnswers(prisma)({ fakeStorage, resultIdPrefix, count })
} }
const createAnswers = const createAnswers =
(prisma: PrismaClient) => (prisma: PrismaClient) =>
({ ({
count, count,
idPrefix, resultIdPrefix,
fakeStorage, fakeStorage,
}: Pick<CreateFakeResultsProps, 'fakeStorage' | 'idPrefix' | 'count'>) => { }: { resultIdPrefix: string } & Pick<
CreateFakeResultsProps,
'fakeStorage' | 'count'
>) => {
return prisma.answer.createMany({ return prisma.answer.createMany({
data: [ data: [
...Array.from(Array(count)).map((_, idx) => ({ ...Array.from(Array(count)).map((_, idx) => ({
resultId: `${idPrefix}-result${idx}`, resultId: `${resultIdPrefix}-result${idx}`,
content: `content${idx}`, content: `content${idx}`,
blockId: 'block1', blockId: 'block1',
groupId: 'block1', groupId: 'block1',

2141
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff