From bfed59969599abc5a844540bd981f7dca7435f0e Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 6 Oct 2022 08:33:46 +0200 Subject: [PATCH] :sparkles: (editor) Add unpublish and close typebot options Introducing more menu items on the "Publised" button in the editor. You can now unpublish a typebot and close it to new responses --- apps/builder/assets/icons.tsx | 7 + .../shared/buttons/PublishButton.tsx | 55 ++- .../TypebotContext/TypebotContext.tsx | 19 + apps/builder/pages/api/publicTypebots/[id].ts | 14 +- .../builder/pages/api/typebots/[typebotId].ts | 11 +- apps/builder/playwright/global-setup.ts | 2 +- apps/builder/playwright/services/browser.ts | 13 - apps/builder/playwright/services/database.ts | 391 ------------------ .../playwright/services/databaseActions.ts | 76 ++++ .../playwright/services/selectorUtils.ts | 3 - .../playwright/tests/accountSettings.spec.ts | 2 +- .../playwright/tests/analytics.spec.ts | 6 +- apps/builder/playwright/tests/billing.spec.ts | 10 +- .../playwright/tests/bubbles/embed.spec.ts | 8 +- .../playwright/tests/bubbles/image.spec.ts | 8 +- .../playwright/tests/bubbles/text.spec.ts | 8 +- .../playwright/tests/bubbles/video.spec.ts | 8 +- .../playwright/tests/collaboration.spec.ts | 12 +- .../playwright/tests/customDomains.spec.ts | 8 +- .../playwright/tests/dashboard.spec.ts | 5 +- apps/builder/playwright/tests/editor.spec.ts | 56 ++- .../playwright/tests/inputs/buttons.spec.ts | 6 +- .../playwright/tests/inputs/date.spec.ts | 8 +- .../playwright/tests/inputs/email.spec.ts | 8 +- .../playwright/tests/inputs/file.spec.ts | 10 +- .../playwright/tests/inputs/number.spec.ts | 8 +- .../playwright/tests/inputs/payment.spec.ts | 9 +- .../playwright/tests/inputs/phone.spec.ts | 8 +- .../playwright/tests/inputs/rating.spec.ts | 8 +- .../playwright/tests/inputs/text.spec.ts | 8 +- .../playwright/tests/inputs/url.spec.ts | 8 +- .../integrations/googleAnalytics.spec.ts | 6 +- .../tests/integrations/googleSheets.spec.ts | 4 +- .../tests/integrations/sendEmail.spec.ts | 4 +- .../tests/integrations/webhook.spec.ts | 5 +- .../playwright/tests/logic/code.spec.ts | 4 +- .../playwright/tests/logic/condition.spec.ts | 4 +- .../playwright/tests/logic/redirect.spec.ts | 4 +- .../tests/logic/setVariable.spec.ts | 4 +- .../tests/logic/typebotLink.spec.ts | 4 +- apps/builder/playwright/tests/results.spec.ts | 17 +- .../builder/playwright/tests/settings.spec.ts | 5 +- .../playwright/tests/templates.spec.ts | 2 +- apps/builder/playwright/tests/theme.spec.ts | 4 +- .../playwright/tests/workspaces.spec.ts | 8 +- apps/builder/services/publicTypebot.tsx | 2 + .../typebots/deletePublishedTypebotQuery.ts | 13 + apps/builder/services/typebots/typebots.ts | 2 + apps/viewer/layouts/TypebotPage.tsx | 24 +- apps/viewer/package.json | 1 + apps/viewer/pages/[[...publicId]].tsx | 60 ++- apps/viewer/playwright/global-setup.ts | 2 +- apps/viewer/playwright/services/browser.ts | 12 - apps/viewer/playwright/services/database.ts | 239 ----------- .../playwright/services/databaseActions.ts | 23 ++ .../playwright/services/selectorUtils.ts | 4 - apps/viewer/playwright/tests/api.spec.ts | 12 +- .../playwright/tests/fileUpload.spec.ts | 4 +- .../viewer/playwright/tests/hugeBlock.spec.ts | 4 +- apps/viewer/playwright/tests/limits.spec.ts | 5 - apps/viewer/playwright/tests/metadata.spec.ts | 8 +- .../tests/predefinedVariables.spec.ts | 4 +- .../viewer/playwright/tests/sendEmail.spec.ts | 8 +- apps/viewer/playwright/tests/settings.spec.ts | 23 +- .../playwright/tests/typebotLink.spec.ts | 4 +- apps/viewer/playwright/tests/webhook.spec.ts | 7 +- .../src/components/TypebotViewer.tsx | 2 +- .../src/contexts/TypebotContext.tsx | 8 +- packages/bot-engine/src/services/logic.ts | 5 +- .../migration.sql | 2 + packages/db/prisma/schema.prisma | 3 +- packages/models/typebot/typebot.ts | 2 + packages/utils/index.ts | 1 - packages/utils/package.json | 1 + packages/utils/playwright.ts | 65 --- packages/utils/playwright/databaseActions.ts | 168 ++++++++ packages/utils/playwright/databaseHelpers.ts | 86 ++++ packages/utils/playwright/databaseSetup.ts | 146 +++++++ packages/utils/playwright/testHelpers.ts | 30 ++ pnpm-lock.yaml | 213 +++++++++- 80 files changed, 1111 insertions(+), 960 deletions(-) delete mode 100644 apps/builder/playwright/services/database.ts create mode 100644 apps/builder/playwright/services/databaseActions.ts create mode 100644 apps/builder/services/typebots/deletePublishedTypebotQuery.ts delete mode 100644 apps/viewer/playwright/services/browser.ts delete mode 100644 apps/viewer/playwright/services/database.ts create mode 100644 apps/viewer/playwright/services/databaseActions.ts delete mode 100644 apps/viewer/playwright/services/selectorUtils.ts create mode 100644 packages/db/prisma/migrations/20221006063227_add_is_closed_to_typebot/migration.sql delete mode 100644 packages/utils/playwright.ts create mode 100644 packages/utils/playwright/databaseActions.ts create mode 100644 packages/utils/playwright/databaseHelpers.ts create mode 100644 packages/utils/playwright/databaseSetup.ts create mode 100644 packages/utils/playwright/testHelpers.ts diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index 98f2137cf92..da586c66315 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -489,3 +489,10 @@ export const AlertIcon = (props: IconProps) => ( ) + +export const CloudOffIcon = (props: IconProps) => ( + + + + +) diff --git a/apps/builder/components/shared/buttons/PublishButton.tsx b/apps/builder/components/shared/buttons/PublishButton.tsx index 25da59a8497..e82951b424c 100644 --- a/apps/builder/components/shared/buttons/PublishButton.tsx +++ b/apps/builder/components/shared/buttons/PublishButton.tsx @@ -12,7 +12,12 @@ import { useDisclosure, ButtonProps, } from '@chakra-ui/react' -import { ChevronLeftIcon } from 'assets/icons' +import { + ChevronLeftIcon, + CloudOffIcon, + LockedIcon, + UnlockedIcon, +} from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useWorkspace } from 'contexts/WorkspaceContext' import { InputBlockType } from 'models' @@ -34,6 +39,9 @@ export const PublishButton = (props: ButtonProps) => { restorePublishedTypebot, typebot, isSavingLoading, + updateTypebot, + unpublishTypebot, + save, } = useTypebot() const hasInputFile = typebot?.groups @@ -46,6 +54,16 @@ export const PublishButton = (props: ButtonProps) => { if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`) } + const closeTypebot = async () => { + updateTypebot({ isClosed: true }) + await save() + } + + const openTypebot = async () => { + updateTypebot({ isClosed: false }) + await save() + } + return ( { } - isDisabled={isNotDefined(publishedTypebot)} + isDisabled={isNotDefined(publishedTypebot) || isPublished} > - {publishedTypebot && !isPublished && ( + {publishedTypebot && ( } - aria-label="Show published version" + aria-label="Show published typebot menu" size="sm" + isDisabled={isPublishing || isSavingLoading} /> - - Restore published version + {!isPublished && ( + + Restore published version + + )} + {!typebot?.isClosed ? ( + }> + Close typebot to new responses + + ) : ( + }> + Reopen typebot to new responses + + )} + }> + Unpublish typebot diff --git a/apps/builder/contexts/TypebotContext/TypebotContext.tsx b/apps/builder/contexts/TypebotContext/TypebotContext.tsx index 945f4d8a2a3..f1c2ec422f0 100644 --- a/apps/builder/contexts/TypebotContext/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext/TypebotContext.tsx @@ -44,6 +44,7 @@ import { saveWebhook } from 'services/webhook' import { stringify } from 'qs' import cuid from 'cuid' import { useToast } from 'components/shared/hooks/useToast' +import { deletePublishedTypebotQuery } from 'services/typebots/deletePublishedTypebotQuery' const autoSaveTimeout = 10000 type UpdateTypebotPayload = Partial<{ @@ -55,6 +56,7 @@ type UpdateTypebotPayload = Partial<{ icon: string customDomain: string resultsTablePreferences: ResultsTablePreferences + isClosed: boolean }> export type SetTypebot = ( @@ -81,6 +83,7 @@ const typebotContext = createContext< ) => Promise updateTypebot: (updates: UpdateTypebotPayload) => void publishTypebot: () => void + unpublishTypebot: () => void restorePublishedTypebot: () => void } & GroupsActions & BlocksActions & @@ -314,6 +317,21 @@ export const TypebotContext = ({ } } + const unpublishTypebot = async () => { + if (!publishedTypebot || !localTypebot) return + setIsPublishing(true) + const { error } = await deletePublishedTypebotQuery({ + publishedTypebotId: publishedTypebot.id, + typebotId: localTypebot.id, + }) + setIsPublishing(false) + if (error) showToast({ description: error.message }) + mutate({ + typebot: localTypebot, + webhooks: webhooks ?? [], + }) + } + const restorePublishedTypebot = () => { if (!publishedTypebot || !localTypebot) return setLocalTypebot(parsePublicTypebotToTypebot(publishedTypebot, localTypebot)) @@ -351,6 +369,7 @@ export const TypebotContext = ({ canUndo, canRedo, publishTypebot, + unpublishTypebot, isPublishing, isPublished, updateTypebot: updateLocalTypebot, diff --git a/apps/builder/pages/api/publicTypebots/[id].ts b/apps/builder/pages/api/publicTypebots/[id].ts index c40b371c840..d8cafe1a850 100644 --- a/apps/builder/pages/api/publicTypebots/[id].ts +++ b/apps/builder/pages/api/publicTypebots/[id].ts @@ -2,7 +2,7 @@ import { withSentry } from '@sentry/nextjs' import prisma from 'libs/prisma' import { InputBlockType, PublicTypebot } from 'models' import { NextApiRequest, NextApiResponse } from 'next' -import { canPublishFileInput } from 'services/api/dbRules' +import { canPublishFileInput, canWriteTypebot } from 'services/api/dbRules' import { getAuthenticatedUser } from 'services/api/utils' import { badRequest, methodNotAllowed, notAuthenticated } from 'utils/api' @@ -32,6 +32,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) return res.send({ typebots }) } + if (req.method === 'DELETE') { + const publishedTypebotId = req.query.id as string + const typebotId = req.query.typebotId as string | undefined + if (!typebotId) return badRequest(res, 'typebotId is required') + await prisma.publicTypebot.deleteMany({ + where: { + id: publishedTypebotId, + typebot: canWriteTypebot(typebotId, user), + }, + }) + return res.send({ success: true }) + } return methodNotAllowed(res) } diff --git a/apps/builder/pages/api/typebots/[typebotId].ts b/apps/builder/pages/api/typebots/[typebotId].ts index 668318c15e2..01f3a07a329 100644 --- a/apps/builder/pages/api/typebots/[typebotId].ts +++ b/apps/builder/pages/api/typebots/[typebotId].ts @@ -38,15 +38,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === 'DELETE') { - const typebots = await prisma.typebot.updateMany({ - where: canWriteTypebot(typebotId, user), - data: { isArchived: true }, - }) await archiveResults(res)({ typebotId, user, resultsFilter: { typebotId }, }) + await prisma.publicTypebot.deleteMany({ + where: { typebot: canWriteTypebot(typebotId, user) }, + }) + const typebots = await prisma.typebot.updateMany({ + where: canWriteTypebot(typebotId, user), + data: { isArchived: true }, + }) return res.send({ typebots }) } if (req.method === 'PUT') { diff --git a/apps/builder/playwright/global-setup.ts b/apps/builder/playwright/global-setup.ts index d32ac00308c..60711f0403d 100644 --- a/apps/builder/playwright/global-setup.ts +++ b/apps/builder/playwright/global-setup.ts @@ -1,5 +1,5 @@ import { FullConfig } from '@playwright/test' -import { setupDatabase, teardownDatabase } from './services/database' +import { setupDatabase, teardownDatabase } from 'utils/playwright/databaseSetup' // eslint-disable-next-line @typescript-eslint/no-var-requires require('dotenv').config({ path: '.env' }) diff --git a/apps/builder/playwright/services/browser.ts b/apps/builder/playwright/services/browser.ts index 96e9055347f..245a9d284d6 100644 --- a/apps/builder/playwright/services/browser.ts +++ b/apps/builder/playwright/services/browser.ts @@ -1,18 +1,5 @@ -import { Page } from '@playwright/test' - export const refreshUser = async () => { await fetch('/api/auth/session?update') const event = new Event('visibilitychange') document.dispatchEvent(event) } - -export const mockSessionResponsesToOtherUser = async (page: Page) => - page.route('/api/auth/session', (route) => { - if (route.request().method() === 'GET') { - return route.fulfill({ - status: 200, - 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() - }) diff --git a/apps/builder/playwright/services/database.ts b/apps/builder/playwright/services/database.ts deleted file mode 100644 index 057b01a4427..00000000000 --- a/apps/builder/playwright/services/database.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { - CredentialsType, - defaultSettings, - defaultTheme, - PublicTypebot, - Block, - Typebot, - Webhook, -} from 'models' -import { - CollaborationType, - DashboardFolder, - GraphNavigation, - Plan, - PrismaClient, - User, - WorkspaceRole, - Workspace, -} from 'db' -import { readFileSync } from 'fs' -import { injectFakeResults } from 'utils' -import { encrypt } from 'utils/api' -import Stripe from 'stripe' -import cuid from 'cuid' - -const prisma = new PrismaClient() - -const stripe = new Stripe(process.env.STRIPE_TEST_SECRET_KEY ?? '', { - apiVersion: '2022-08-01', -}) - -export const userId = 'userId' -const otherUserId = 'otherUserId' -export const freeWorkspaceId = 'freeWorkspace' -export const starterWorkspaceId = 'starterWorkspace' -export const proWorkspaceId = 'proWorkspace' -const lifetimeWorkspaceId = 'lifetimeWorkspaceId' - -export const teardownDatabase = async () => { - await prisma.workspace.deleteMany({ - where: { - members: { - some: { userId: { in: [userId, otherUserId] } }, - }, - }, - }) - await prisma.user.deleteMany({ - where: { id: { in: [userId, otherUserId] } }, - }) - return prisma.webhook.deleteMany() -} - -export const deleteWorkspaces = async (workspaceIds: string[]) => { - await prisma.workspace.deleteMany({ - where: { id: { in: workspaceIds } }, - }) -} - -export const addSubscriptionToWorkspace = async ( - workspaceId: string, - items: Stripe.SubscriptionCreateParams.Item[], - metadata: Pick< - Workspace, - 'additionalChatsIndex' | 'additionalStorageIndex' | 'plan' - > -) => { - const { id: stripeId } = await stripe.customers.create({ - email: 'test-user@gmail.com', - name: 'Test User', - }) - const { id: paymentId } = await stripe.paymentMethods.create({ - card: { - number: '4242424242424242', - exp_month: 12, - exp_year: 2022, - cvc: '123', - }, - type: 'card', - }) - await stripe.paymentMethods.attach(paymentId, { customer: stripeId }) - await stripe.subscriptions.create({ - customer: stripeId, - items, - default_payment_method: paymentId, - currency: 'eur', - }) - await stripe.customers.update(stripeId, { - invoice_settings: { default_payment_method: paymentId }, - }) - await prisma.workspace.update({ - where: { id: workspaceId }, - data: { - stripeId, - ...metadata, - }, - }) -} - -export const setupDatabase = async () => { - await setupWorkspaces() - await setupUsers() - return setupCredentials() -} - -export const setupWorkspaces = async () => { - await prisma.workspace.create({ - data: { - id: freeWorkspaceId, - name: 'Free workspace', - plan: Plan.FREE, - }, - }) - await prisma.workspace.createMany({ - data: [ - { - id: starterWorkspaceId, - name: 'Starter workspace', - stripeId: 'cus_LnPDugJfa18N41', - plan: Plan.STARTER, - }, - { - id: proWorkspaceId, - name: 'Pro workspace', - plan: Plan.PRO, - }, - { - id: lifetimeWorkspaceId, - name: 'Lifetime workspace', - plan: Plan.LIFETIME, - }, - ], - }) -} - -export const createWorkspaces = async (workspaces: Partial[]) => { - const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid()) - await prisma.workspace.createMany({ - data: workspaces.map((workspace, index) => ({ - id: workspaceIds[index], - name: 'Free workspace', - plan: Plan.FREE, - ...workspace, - })), - }) - await prisma.memberInWorkspace.createMany({ - data: workspaces.map((_, index) => ({ - userId, - workspaceId: workspaceIds[index], - role: WorkspaceRole.ADMIN, - })), - }) - return workspaceIds -} - -export const setupUsers = async () => { - await prisma.user.create({ - data: { - id: userId, - email: 'user@email.com', - name: 'John Doe', - graphNavigation: GraphNavigation.TRACKPAD, - apiTokens: { - createMany: { - data: [ - { - name: 'Token 1', - token: 'jirowjgrwGREHEtoken1', - 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), - }, - ], - }, - }, - }, - }) - await prisma.user.create({ - data: { id: otherUserId, email: 'other-user@email.com', name: 'James Doe' }, - }) - return prisma.memberInWorkspace.createMany({ - data: [ - { - role: WorkspaceRole.ADMIN, - userId, - workspaceId: freeWorkspaceId, - }, - { - role: WorkspaceRole.ADMIN, - userId, - workspaceId: starterWorkspaceId, - }, - { - role: WorkspaceRole.ADMIN, - userId, - workspaceId: proWorkspaceId, - }, - { - role: WorkspaceRole.ADMIN, - userId, - workspaceId: lifetimeWorkspaceId, - }, - ], - }) -} - -export const createWebhook = async ( - typebotId: string, - webhookProps?: Partial -) => { - try { - await prisma.webhook.delete({ where: { id: 'webhook1' } }) - } catch {} - return prisma.webhook.create({ - data: { method: 'GET', typebotId, id: 'webhook1', ...webhookProps }, - }) -} - -export const createCollaboration = ( - userId: string, - typebotId: string, - type: CollaborationType -) => - prisma.collaboratorsOnTypebots.create({ data: { userId, typebotId, type } }) - -export const getSignedInUser = (email: string) => - prisma.user.findFirst({ where: { email } }) - -export const createTypebots = async (partialTypebots: Partial[]) => { - const typebotsWithId = partialTypebots.map((typebot) => ({ - ...typebot, - id: typebot.id ?? cuid(), - })) - await prisma.typebot.createMany({ - data: typebotsWithId.map(parseTestTypebot), - }) - return prisma.publicTypebot.createMany({ - data: typebotsWithId.map((t) => - parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t)) - ), - }) -} - -export const createFolders = (partialFolders: Partial[]) => - prisma.dashboardFolder.createMany({ - data: partialFolders.map((folder) => ({ - workspaceId: proWorkspaceId, - name: 'Folder #1', - ...folder, - })), - }) - -const setupCredentials = () => { - const { encryptedData, iv } = encrypt({ - expiry_date: 1642441058842, - access_token: - 'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod', - // This token is linked to a test Google account (typebot.test.user@gmail.com) - refresh_token: - '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek', - }) - return prisma.credentials.createMany({ - data: [ - { - name: 'pro-user@email.com', - type: CredentialsType.GOOGLE_SHEETS, - data: encryptedData, - workspaceId: proWorkspaceId, - iv, - }, - ], - }) -} - -export const updateUser = (data: Partial) => - prisma.user.update({ - data, - where: { - id: userId, - }, - }) - -export const createResults = injectFakeResults(prisma) - -export const createFolder = (workspaceId: string, name: string) => - prisma.dashboardFolder.create({ - data: { - workspaceId, - name, - }, - }) - -const parseTypebotToPublicTypebot = ( - id: string, - typebot: Typebot -): Omit => ({ - id, - groups: typebot.groups, - typebotId: typebot.id, - theme: typebot.theme, - settings: typebot.settings, - variables: typebot.variables, - edges: typebot.edges, -}) - -const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ - id: cuid(), - workspaceId: proWorkspaceId, - folderId: null, - name: 'My typebot', - theme: defaultTheme, - settings: defaultSettings, - publicId: null, - updatedAt: new Date().toISOString(), - createdAt: new Date().toISOString(), - publishedTypebotId: null, - customDomain: null, - icon: null, - variables: [{ id: 'var1', name: 'var1' }], - ...partialTypebot, - edges: [ - { - id: 'edge1', - from: { groupId: 'block0', blockId: 'block0' }, - to: { groupId: 'block1' }, - }, - ], - groups: [ - { - id: 'block0', - title: 'Group #0', - blocks: [ - { - id: 'block0', - type: 'start', - groupId: 'block0', - label: 'Start', - outgoingEdgeId: 'edge1', - }, - ], - graphCoordinates: { x: 0, y: 0 }, - }, - ...(partialTypebot.groups ?? []), - ], -}) - -export const parseDefaultGroupWithBlock = ( - block: Partial -): Pick => ({ - groups: [ - { - graphCoordinates: { x: 200, y: 200 }, - id: 'block1', - blocks: [ - { - id: 'block1', - groupId: 'block1', - ...block, - } as Block, - ], - title: 'Group #1', - }, - ], -}) - -export const importTypebotInDatabase = async ( - path: string, - updates?: Partial -) => { - const typebot: Typebot = { - ...JSON.parse(readFileSync(path).toString()), - workspaceId: proWorkspaceId, - ...updates, - } - await prisma.typebot.create({ - data: typebot, - }) - return prisma.publicTypebot.create({ - data: parseTypebotToPublicTypebot( - updates?.id ? `${updates?.id}-public` : 'publicBot', - typebot - ), - }) -} diff --git a/apps/builder/playwright/services/databaseActions.ts b/apps/builder/playwright/services/databaseActions.ts new file mode 100644 index 00000000000..2790d98e857 --- /dev/null +++ b/apps/builder/playwright/services/databaseActions.ts @@ -0,0 +1,76 @@ +import { CollaborationType, DashboardFolder, PrismaClient, Workspace } from 'db' +import Stripe from 'stripe' +import { proWorkspaceId } from 'utils/playwright/databaseSetup' + +const prisma = new PrismaClient() + +const stripe = new Stripe(process.env.STRIPE_TEST_SECRET_KEY ?? '', { + apiVersion: '2022-08-01', +}) + +export const addSubscriptionToWorkspace = async ( + workspaceId: string, + items: Stripe.SubscriptionCreateParams.Item[], + metadata: Pick< + Workspace, + 'additionalChatsIndex' | 'additionalStorageIndex' | 'plan' + > +) => { + const { id: stripeId } = await stripe.customers.create({ + email: 'test-user@gmail.com', + name: 'Test User', + }) + const { id: paymentId } = await stripe.paymentMethods.create({ + card: { + number: '4242424242424242', + exp_month: 12, + exp_year: 2022, + cvc: '123', + }, + type: 'card', + }) + await stripe.paymentMethods.attach(paymentId, { customer: stripeId }) + await stripe.subscriptions.create({ + customer: stripeId, + items, + default_payment_method: paymentId, + currency: 'eur', + }) + await stripe.customers.update(stripeId, { + invoice_settings: { default_payment_method: paymentId }, + }) + await prisma.workspace.update({ + where: { id: workspaceId }, + data: { + stripeId, + ...metadata, + }, + }) +} + +export const createCollaboration = ( + userId: string, + typebotId: string, + type: CollaborationType +) => + prisma.collaboratorsOnTypebots.create({ data: { userId, typebotId, type } }) + +export const getSignedInUser = (email: string) => + prisma.user.findFirst({ where: { email } }) + +export const createFolders = (partialFolders: Partial[]) => + prisma.dashboardFolder.createMany({ + data: partialFolders.map((folder) => ({ + workspaceId: proWorkspaceId, + name: 'Folder #1', + ...folder, + })), + }) + +export const createFolder = (workspaceId: string, name: string) => + prisma.dashboardFolder.create({ + data: { + workspaceId, + name, + }, + }) diff --git a/apps/builder/playwright/services/selectorUtils.ts b/apps/builder/playwright/services/selectorUtils.ts index ca894900cb7..5c276ce81ef 100644 --- a/apps/builder/playwright/services/selectorUtils.ts +++ b/apps/builder/playwright/services/selectorUtils.ts @@ -3,9 +3,6 @@ import { Page } from '@playwright/test' export const deleteButtonInConfirmDialog = (page: Page) => page.locator('section[role="alertdialog"] button:has-text("Delete")') -export const typebotViewer = (page: Page) => - page.frameLocator('#typebot-iframe') - export const stripePaymentForm = (page: Page) => page .frameLocator('#typebot-iframe') diff --git a/apps/builder/playwright/tests/accountSettings.spec.ts b/apps/builder/playwright/tests/accountSettings.spec.ts index 99daaf04273..abf8d0beba6 100644 --- a/apps/builder/playwright/tests/accountSettings.spec.ts +++ b/apps/builder/playwright/tests/accountSettings.spec.ts @@ -1,6 +1,6 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { userId } from 'playwright/services/database' +import { userId } from 'utils/playwright/databaseSetup' test.describe.configure({ mode: 'parallel' }) diff --git a/apps/builder/playwright/tests/analytics.spec.ts b/apps/builder/playwright/tests/analytics.spec.ts index 500e888acf0..8ff41c598f6 100644 --- a/apps/builder/playwright/tests/analytics.spec.ts +++ b/apps/builder/playwright/tests/analytics.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import path from 'path' -import { - importTypebotInDatabase, - starterWorkspaceId, -} from '../services/database' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { starterWorkspaceId } from 'utils/playwright/databaseSetup' test('analytics are not available for non-pro workspaces', async ({ page }) => { const typebotId = cuid() diff --git a/apps/builder/playwright/tests/billing.spec.ts b/apps/builder/playwright/tests/billing.spec.ts index 251222f424f..8f106312e99 100644 --- a/apps/builder/playwright/tests/billing.spec.ts +++ b/apps/builder/playwright/tests/billing.spec.ts @@ -1,13 +1,13 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import { Plan } from 'db' +import { addSubscriptionToWorkspace } from 'playwright/services/databaseActions' import { - addSubscriptionToWorkspace, - createResults, createTypebots, createWorkspaces, deleteWorkspaces, -} from '../services/database' + injectFakeResults, +} from 'utils/playwright/databaseActions' const usageWorkspaceId = cuid() const usageTypebotId = cuid() @@ -48,7 +48,7 @@ test('should display valid usage', async ({ page }) => { await expect(page.locator('text="Storage"')).toBeHidden() await page.click('text=Free workspace', { force: true }) - await createResults({ + await injectFakeResults({ count: 10, typebotId: usageTypebotId, fakeStorage: 1100 * 1024 * 1024, @@ -70,7 +70,7 @@ test('should display valid usage', async ({ page }) => { '54' ) - await createResults({ + await injectFakeResults({ typebotId: usageTypebotId, count: 1090, fakeStorage: 1200 * 1024 * 1024, diff --git a/apps/builder/playwright/tests/bubbles/embed.spec.ts b/apps/builder/playwright/tests/bubbles/embed.spec.ts index 3d63d0c67ed..46a9cf3b53e 100644 --- a/apps/builder/playwright/tests/bubbles/embed.spec.ts +++ b/apps/builder/playwright/tests/bubbles/embed.spec.ts @@ -1,11 +1,9 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' import { BubbleBlockType, defaultEmbedBubbleContent } from 'models' -import { typebotViewer } from '../../services/selectorUtils' import cuid from 'cuid' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' +import { typebotViewer } from 'utils/playwright/testHelpers' const pdfSrc = 'https://www.orimi.com/pdf-test.pdf' const siteSrc = 'https://app.cal.com/baptistearno/15min' diff --git a/apps/builder/playwright/tests/bubbles/image.spec.ts b/apps/builder/playwright/tests/bubbles/image.spec.ts index 7f13a4881fc..33ffc3d09bf 100644 --- a/apps/builder/playwright/tests/bubbles/image.spec.ts +++ b/apps/builder/playwright/tests/bubbles/image.spec.ts @@ -1,12 +1,10 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { BubbleBlockType, defaultImageBubbleContent } from 'models' -import { typebotViewer } from '../../services/selectorUtils' import path from 'path' import cuid from 'cuid' +import { typebotViewer } from 'utils/playwright/testHelpers' const unsplashImageSrc = 'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80' diff --git a/apps/builder/playwright/tests/bubbles/text.spec.ts b/apps/builder/playwright/tests/bubbles/text.spec.ts index 85f84e7d73e..4fa4f7faaa3 100644 --- a/apps/builder/playwright/tests/bubbles/text.spec.ts +++ b/apps/builder/playwright/tests/bubbles/text.spec.ts @@ -1,11 +1,9 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { BubbleBlockType, defaultTextBubbleContent } from 'models' -import { typebotViewer } from '../../services/selectorUtils' import cuid from 'cuid' +import { typebotViewer } from 'utils/playwright/testHelpers' test.describe('Text bubble block', () => { test('rich text features should work', async ({ page }) => { diff --git a/apps/builder/playwright/tests/bubbles/video.spec.ts b/apps/builder/playwright/tests/bubbles/video.spec.ts index 4c8fffd8838..da4e313683b 100644 --- a/apps/builder/playwright/tests/bubbles/video.spec.ts +++ b/apps/builder/playwright/tests/bubbles/video.spec.ts @@ -1,15 +1,13 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { BubbleBlockType, defaultVideoBubbleContent, VideoBubbleContentType, } from 'models' -import { typebotViewer } from '../../services/selectorUtils' import cuid from 'cuid' +import { typebotViewer } from 'utils/playwright/testHelpers' const videoSrc = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4' diff --git a/apps/builder/playwright/tests/collaboration.spec.ts b/apps/builder/playwright/tests/collaboration.spec.ts index 30fda9f648d..b9c5b0f6962 100644 --- a/apps/builder/playwright/tests/collaboration.spec.ts +++ b/apps/builder/playwright/tests/collaboration.spec.ts @@ -3,13 +3,13 @@ import cuid from 'cuid' import { CollaborationType, Plan, WorkspaceRole } from 'db' import prisma from 'libs/prisma' import { InputBlockType, defaultTextInputOptions } from 'models' +import { createFolder } from 'playwright/services/databaseActions' import { - createFolder, - createResults, createTypebots, - parseDefaultGroupWithBlock, - userId, -} from '../services/database' + injectFakeResults, +} from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' +import { userId } from 'utils/playwright/databaseSetup' test.describe('Typebot owner', () => { test('Can invite collaborators', async ({ page }) => { @@ -103,7 +103,7 @@ test.describe('Guest', () => { }, }) await createFolder(guestWorkspaceId, 'Guest folder') - await createResults({ typebotId, count: 10 }) + await injectFakeResults({ typebotId, count: 10 }) await page.goto(`/typebots`) await page.click('text=Pro workspace') await page.click('text=Guest workspace #2') diff --git a/apps/builder/playwright/tests/customDomains.spec.ts b/apps/builder/playwright/tests/customDomains.spec.ts index e7227342777..12c5fa7a351 100644 --- a/apps/builder/playwright/tests/customDomains.spec.ts +++ b/apps/builder/playwright/tests/customDomains.spec.ts @@ -1,11 +1,9 @@ import test, { expect } from '@playwright/test' import { InputBlockType, defaultTextInputOptions } from 'models' -import { - createTypebots, - parseDefaultGroupWithBlock, - starterWorkspaceId, -} from '../services/database' import cuid from 'cuid' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' +import { starterWorkspaceId } from 'utils/playwright/databaseSetup' test('should be able to connect custom domain', async ({ page }) => { const typebotId = cuid() diff --git a/apps/builder/playwright/tests/dashboard.spec.ts b/apps/builder/playwright/tests/dashboard.spec.ts index f2430d00909..73b668eee8c 100644 --- a/apps/builder/playwright/tests/dashboard.spec.ts +++ b/apps/builder/playwright/tests/dashboard.spec.ts @@ -1,6 +1,7 @@ -import test, { expect, Page } from '@playwright/test' +import test, { expect } from '@playwright/test' import cuid from 'cuid' -import { createFolders, createTypebots } from '../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { createFolders } from '../services/databaseActions' import { deleteButtonInConfirmDialog } from '../services/selectorUtils' test('folders navigation should work', async ({ page }) => { diff --git a/apps/builder/playwright/tests/editor.spec.ts b/apps/builder/playwright/tests/editor.spec.ts index 7965d46bf58..1a765ca769e 100644 --- a/apps/builder/playwright/tests/editor.spec.ts +++ b/apps/builder/playwright/tests/editor.spec.ts @@ -1,13 +1,18 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - importTypebotInDatabase, - parseDefaultGroupWithBlock, -} from '../services/database' import { defaultTextInputOptions, InputBlockType } from 'models' import path from 'path' import cuid from 'cuid' -import { typebotViewer } from '../services/selectorUtils' +import { + createTypebots, + importTypebotInDatabase, +} from 'utils/playwright/databaseActions' +import { + typebotViewer, + waitForSuccessfulDeleteRequest, + waitForSuccessfulPostRequest, + waitForSuccessfulPutRequest, +} from 'utils/playwright/testHelpers' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' test.describe.configure({ mode: 'parallel' }) @@ -180,3 +185,42 @@ test('Preview from group should work', async ({ page }) => { typebotViewer(page).locator('text="Hello this is group 1"') ).toBeVisible() }) + +test('Published typebot menu 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 expect(page.locator("text='Start'")).toBeVisible() + await expect(page.locator('button >> text="Published"')).toBeVisible() + await page.click('[aria-label="Show published typebot menu"]') + await Promise.all([ + waitForSuccessfulPutRequest(page), + page.click('text="Close typebot to new responses"'), + ]) + await expect(page.locator('button >> text="Closed"')).toBeDisabled() + await page.click('[aria-label="Show published typebot menu"]') + await Promise.all([ + waitForSuccessfulPutRequest(page), + page.click('text="Reopen typebot to new responses"'), + ]) + await expect(page.locator('button >> text="Published"')).toBeDisabled() + await page.click('[aria-label="Show published typebot menu"]') + await Promise.all([ + waitForSuccessfulDeleteRequest(page), + page.click('button >> text="Unpublish typebot"'), + ]) + await Promise.all([ + waitForSuccessfulPostRequest(page), + page.click('button >> text="Publish"'), + ]) + await expect(page.locator('button >> text="Published"')).toBeVisible() +}) diff --git a/apps/builder/playwright/tests/inputs/buttons.spec.ts b/apps/builder/playwright/tests/inputs/buttons.spec.ts index f93820c255a..e3ba7db58d8 100644 --- a/apps/builder/playwright/tests/inputs/buttons.spec.ts +++ b/apps/builder/playwright/tests/inputs/buttons.spec.ts @@ -2,12 +2,12 @@ import test, { expect } from '@playwright/test' import { createTypebots, importTypebotInDatabase, - parseDefaultGroupWithBlock, -} from '../../services/database' +} from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultChoiceInputOptions, InputBlockType, ItemType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' import cuid from 'cuid' import path from 'path' +import { typebotViewer } from 'utils/playwright/testHelpers' test.describe.parallel('Buttons input block', () => { test('can edit button items', async ({ page }) => { diff --git a/apps/builder/playwright/tests/inputs/date.spec.ts b/apps/builder/playwright/tests/inputs/date.spec.ts index d2f84f3d022..fefc173ff91 100644 --- a/apps/builder/playwright/tests/inputs/date.spec.ts +++ b/apps/builder/playwright/tests/inputs/date.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultDateInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe('Date input block', () => { diff --git a/apps/builder/playwright/tests/inputs/email.spec.ts b/apps/builder/playwright/tests/inputs/email.spec.ts index 1e2a7c2fb4a..40bd17b418f 100644 --- a/apps/builder/playwright/tests/inputs/email.spec.ts +++ b/apps/builder/playwright/tests/inputs/email.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultEmailInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe('Email input block', () => { diff --git a/apps/builder/playwright/tests/inputs/file.spec.ts b/apps/builder/playwright/tests/inputs/file.spec.ts index 7db06f3436b..4e61de7ab6b 100644 --- a/apps/builder/playwright/tests/inputs/file.spec.ts +++ b/apps/builder/playwright/tests/inputs/file.spec.ts @@ -1,13 +1,11 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - freeWorkspaceId, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultFileInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' import path from 'path' +import { freeWorkspaceId } from 'utils/playwright/databaseSetup' test.describe.configure({ mode: 'parallel' }) diff --git a/apps/builder/playwright/tests/inputs/number.spec.ts b/apps/builder/playwright/tests/inputs/number.spec.ts index b67a31d52b2..892d2469165 100644 --- a/apps/builder/playwright/tests/inputs/number.spec.ts +++ b/apps/builder/playwright/tests/inputs/number.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultNumberInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe('Number input block', () => { diff --git a/apps/builder/playwright/tests/inputs/payment.spec.ts b/apps/builder/playwright/tests/inputs/payment.spec.ts index 393a512cfa6..756cab3172a 100644 --- a/apps/builder/playwright/tests/inputs/payment.spec.ts +++ b/apps/builder/playwright/tests/inputs/payment.spec.ts @@ -1,11 +1,10 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultPaymentInputOptions, InputBlockType } from 'models' import cuid from 'cuid' -import { stripePaymentForm, typebotViewer } from '../../services/selectorUtils' +import { stripePaymentForm } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' test.describe('Payment input block', () => { test('Can configure Stripe account', async ({ page }) => { diff --git a/apps/builder/playwright/tests/inputs/phone.spec.ts b/apps/builder/playwright/tests/inputs/phone.spec.ts index 3a49473d155..b882b596fc8 100644 --- a/apps/builder/playwright/tests/inputs/phone.spec.ts +++ b/apps/builder/playwright/tests/inputs/phone.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultPhoneInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe('Phone input block', () => { diff --git a/apps/builder/playwright/tests/inputs/rating.spec.ts b/apps/builder/playwright/tests/inputs/rating.spec.ts index 0c6f50e52a1..3e58884ed86 100644 --- a/apps/builder/playwright/tests/inputs/rating.spec.ts +++ b/apps/builder/playwright/tests/inputs/rating.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultRatingInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' const boxSvg = ` { diff --git a/apps/builder/playwright/tests/inputs/url.spec.ts b/apps/builder/playwright/tests/inputs/url.spec.ts index eacc289b7a8..826bf787d3d 100644 --- a/apps/builder/playwright/tests/inputs/url.spec.ts +++ b/apps/builder/playwright/tests/inputs/url.spec.ts @@ -1,10 +1,8 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultUrlInputOptions, InputBlockType } from 'models' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe('Url input block', () => { diff --git a/apps/builder/playwright/tests/integrations/googleAnalytics.spec.ts b/apps/builder/playwright/tests/integrations/googleAnalytics.spec.ts index a900d4a92a0..99d2d0dfbb6 100644 --- a/apps/builder/playwright/tests/integrations/googleAnalytics.spec.ts +++ b/apps/builder/playwright/tests/integrations/googleAnalytics.spec.ts @@ -1,8 +1,6 @@ import test from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../../services/database' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { defaultGoogleAnalyticsOptions, IntegrationBlockType } from 'models' import cuid from 'cuid' diff --git a/apps/builder/playwright/tests/integrations/googleSheets.spec.ts b/apps/builder/playwright/tests/integrations/googleSheets.spec.ts index f29554c2a90..158633e97b2 100644 --- a/apps/builder/playwright/tests/integrations/googleSheets.spec.ts +++ b/apps/builder/playwright/tests/integrations/googleSheets.spec.ts @@ -1,7 +1,7 @@ import test, { expect, Page } from '@playwright/test' -import { importTypebotInDatabase } from '../../services/database' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' test.describe.parallel('Google sheets integration', () => { diff --git a/apps/builder/playwright/tests/integrations/sendEmail.spec.ts b/apps/builder/playwright/tests/integrations/sendEmail.spec.ts index af216c78648..aa94fd99ba8 100644 --- a/apps/builder/playwright/tests/integrations/sendEmail.spec.ts +++ b/apps/builder/playwright/tests/integrations/sendEmail.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' -import { importTypebotInDatabase } from '../../services/database' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' import cuid from 'cuid' const typebotId = cuid() diff --git a/apps/builder/playwright/tests/integrations/webhook.spec.ts b/apps/builder/playwright/tests/integrations/webhook.spec.ts index 8e3d4e8c42f..c17972abacd 100644 --- a/apps/builder/playwright/tests/integrations/webhook.spec.ts +++ b/apps/builder/playwright/tests/integrations/webhook.spec.ts @@ -1,5 +1,8 @@ import test, { expect, Page } from '@playwright/test' -import { createWebhook, importTypebotInDatabase } from '../../services/database' +import { + createWebhook, + importTypebotInDatabase, +} from 'utils/playwright/databaseActions' import path from 'path' import { HttpMethod } from 'models' import cuid from 'cuid' diff --git a/apps/builder/playwright/tests/logic/code.spec.ts b/apps/builder/playwright/tests/logic/code.spec.ts index 17a1f86f12c..9ad09689b04 100644 --- a/apps/builder/playwright/tests/logic/code.spec.ts +++ b/apps/builder/playwright/tests/logic/code.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' -import { importTypebotInDatabase } from '../../services/database' +import { typebotViewer } from 'utils/playwright/testHelpers' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import cuid from 'cuid' const typebotId = cuid() diff --git a/apps/builder/playwright/tests/logic/condition.spec.ts b/apps/builder/playwright/tests/logic/condition.spec.ts index 5f9c9effa24..ce0a343636a 100644 --- a/apps/builder/playwright/tests/logic/condition.spec.ts +++ b/apps/builder/playwright/tests/logic/condition.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' -import { importTypebotInDatabase } from '../../services/database' +import { typebotViewer } from 'utils/playwright/testHelpers' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import cuid from 'cuid' const typebotId = cuid() diff --git a/apps/builder/playwright/tests/logic/redirect.spec.ts b/apps/builder/playwright/tests/logic/redirect.spec.ts index 838288a5d2c..6fae93e3fd2 100644 --- a/apps/builder/playwright/tests/logic/redirect.spec.ts +++ b/apps/builder/playwright/tests/logic/redirect.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' -import { importTypebotInDatabase } from '../../services/database' +import { typebotViewer } from 'utils/playwright/testHelpers' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import cuid from 'cuid' const typebotId = cuid() diff --git a/apps/builder/playwright/tests/logic/setVariable.spec.ts b/apps/builder/playwright/tests/logic/setVariable.spec.ts index b24141cd4bc..b4e75d68fc9 100644 --- a/apps/builder/playwright/tests/logic/setVariable.spec.ts +++ b/apps/builder/playwright/tests/logic/setVariable.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { typebotViewer } from '../../services/selectorUtils' -import { importTypebotInDatabase } from '../../services/database' +import { typebotViewer } from 'utils/playwright/testHelpers' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import cuid from 'cuid' const typebotId = cuid() diff --git a/apps/builder/playwright/tests/logic/typebotLink.spec.ts b/apps/builder/playwright/tests/logic/typebotLink.spec.ts index 19730ceb9a8..b1c651b96ef 100644 --- a/apps/builder/playwright/tests/logic/typebotLink.spec.ts +++ b/apps/builder/playwright/tests/logic/typebotLink.spec.ts @@ -1,6 +1,6 @@ import test, { expect } from '@playwright/test' -import { typebotViewer } from '../../services/selectorUtils' -import { importTypebotInDatabase } from '../../services/database' +import { typebotViewer } from 'utils/playwright/testHelpers' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' import path from 'path' import cuid from 'cuid' diff --git a/apps/builder/playwright/tests/results.spec.ts b/apps/builder/playwright/tests/results.spec.ts index d753552e24a..9714dccda5b 100644 --- a/apps/builder/playwright/tests/results.spec.ts +++ b/apps/builder/playwright/tests/results.spec.ts @@ -5,11 +5,11 @@ import { defaultTextInputOptions, InputBlockType } from 'models' import { parse } from 'papaparse' import path from 'path' import { - createResults, - createTypebots, importTypebotInDatabase, - parseDefaultGroupWithBlock, -} from '../services/database' + injectFakeResults, + createTypebots, +} from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' import { deleteButtonInConfirmDialog } from '../services/selectorUtils' const typebotId = cuid() @@ -43,7 +43,7 @@ test('results should be deletable', async ({ page }) => { }), }, ]) - await createResults({ typebotId, count: 200, isChronological: true }) + await injectFakeResults({ typebotId, count: 200, isChronological: true }) await page.goto(`/typebots/${typebotId}/results`) await expect(page.locator('text=content199')).toBeVisible() await page.click('[data-testid="checkbox"] >> nth=1') @@ -69,7 +69,7 @@ test('submissions table should have infinite scroll', async ({ page }) => { tableWrapper.scrollTo(0, tableWrapper.scrollHeight) }) - await createResults({ typebotId, count: 200, isChronological: true }) + await injectFakeResults({ typebotId, count: 200, isChronological: true }) await page.goto(`/typebots/${typebotId}/results`) await expect(page.locator('text=content199')).toBeVisible() @@ -182,8 +182,3 @@ const validateExportAll = (data: unknown[]) => { expect((data[1] as unknown[])[1]).toBe('content199') expect((data[200] as unknown[])[1]).toBe('content0') } - -const selectFirstResults = async (page: Page) => { - await page.click('[data-testid="checkbox"] >> nth=1') - await page.click('[data-testid="checkbox"] >> nth=2') -} diff --git a/apps/builder/playwright/tests/settings.spec.ts b/apps/builder/playwright/tests/settings.spec.ts index c8c4f5ad728..50eb0a8e0dd 100644 --- a/apps/builder/playwright/tests/settings.spec.ts +++ b/apps/builder/playwright/tests/settings.spec.ts @@ -2,8 +2,9 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import { defaultTextInputOptions } from 'models' import path from 'path' -import { freeWorkspaceId, importTypebotInDatabase } from '../services/database' -import { typebotViewer } from '../services/selectorUtils' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { freeWorkspaceId } from 'utils/playwright/databaseSetup' +import { typebotViewer } from 'utils/playwright/testHelpers' test.describe.parallel('Settings page', () => { test.describe('General', () => { diff --git a/apps/builder/playwright/tests/templates.spec.ts b/apps/builder/playwright/tests/templates.spec.ts index 4e2608e336d..358269b3fde 100644 --- a/apps/builder/playwright/tests/templates.spec.ts +++ b/apps/builder/playwright/tests/templates.spec.ts @@ -1,6 +1,6 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { typebotViewer } from '../services/selectorUtils' +import { typebotViewer } from 'utils/playwright/testHelpers' test.describe.parallel('Templates page', () => { test('From scratch should create a blank typebot', async ({ page }) => { diff --git a/apps/builder/playwright/tests/theme.spec.ts b/apps/builder/playwright/tests/theme.spec.ts index 186ef50d1d1..9c70fb249c4 100644 --- a/apps/builder/playwright/tests/theme.spec.ts +++ b/apps/builder/playwright/tests/theme.spec.ts @@ -1,8 +1,8 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import path from 'path' -import { importTypebotInDatabase } from '../services/database' -import { typebotViewer } from '../services/selectorUtils' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' const hostAvatarUrl = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80' diff --git a/apps/builder/playwright/tests/workspaces.spec.ts b/apps/builder/playwright/tests/workspaces.spec.ts index 605b5ed7c41..b05e449edc0 100644 --- a/apps/builder/playwright/tests/workspaces.spec.ts +++ b/apps/builder/playwright/tests/workspaces.spec.ts @@ -1,13 +1,13 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import { defaultTextInputOptions, InputBlockType } from 'models' -import { mockSessionResponsesToOtherUser } from 'playwright/services/browser' +import { createTypebots } from 'utils/playwright/databaseActions' import { - createTypebots, - parseDefaultGroupWithBlock, proWorkspaceId, starterWorkspaceId, -} from '../services/database' +} from 'utils/playwright/databaseSetup' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' +import { mockSessionResponsesToOtherUser } from 'utils/playwright/testHelpers' const proTypebotId = cuid() const starterTypebotId = cuid() diff --git a/apps/builder/services/publicTypebot.tsx b/apps/builder/services/publicTypebot.tsx index 6b309501df9..87bd10a0775 100644 --- a/apps/builder/services/publicTypebot.tsx +++ b/apps/builder/services/publicTypebot.tsx @@ -35,6 +35,8 @@ export const parsePublicTypebotToTypebot = ( folderId: existingTypebot.folderId, icon: existingTypebot.icon, workspaceId: existingTypebot.workspaceId, + isArchived: existingTypebot.isArchived, + isClosed: existingTypebot.isClosed, }) export const createPublishedTypebot = async ( diff --git a/apps/builder/services/typebots/deletePublishedTypebotQuery.ts b/apps/builder/services/typebots/deletePublishedTypebotQuery.ts new file mode 100644 index 00000000000..75d24a7fb0c --- /dev/null +++ b/apps/builder/services/typebots/deletePublishedTypebotQuery.ts @@ -0,0 +1,13 @@ +import { sendRequest } from 'utils' + +export const deletePublishedTypebotQuery = ({ + publishedTypebotId, + typebotId, +}: { + publishedTypebotId: string + typebotId: string +}) => + sendRequest({ + method: 'DELETE', + url: `/api/publicTypebots/${publishedTypebotId}?typebotId=${typebotId}`, + }) diff --git a/apps/builder/services/typebots/typebots.ts b/apps/builder/services/typebots/typebots.ts index 7a8440d8152..1088ca06e9d 100644 --- a/apps/builder/services/typebots/typebots.ts +++ b/apps/builder/services/typebots/typebots.ts @@ -415,6 +415,8 @@ export const parseNewTypebot = ({ | 'publicId' | 'customDomain' | 'icon' + | 'isArchived' + | 'isClosed' > => { const startGroupId = cuid() const startBlockId = cuid() diff --git a/apps/viewer/layouts/TypebotPage.tsx b/apps/viewer/layouts/TypebotPage.tsx index 43a99dfbc7e..09971a794b7 100644 --- a/apps/viewer/layouts/TypebotPage.tsx +++ b/apps/viewer/layouts/TypebotPage.tsx @@ -1,5 +1,5 @@ import { TypebotViewer } from 'bot-engine' -import { Answer, PublicTypebot, VariableWithValue } from 'models' +import { Answer, PublicTypebot, Typebot, VariableWithValue } from 'models' import { useRouter } from 'next/router' import React, { useEffect, useState } from 'react' import { upsertAnswer } from 'services/answer' @@ -9,7 +9,9 @@ import { createResult, updateResult } from '../services/result' import { ErrorPage } from './ErrorPage' export type TypebotPageProps = { - typebot?: PublicTypebot & { typebot: { name: string } } + publishedTypebot: Omit & { + typebot: Pick + } url: string isIE: boolean customHeadCode: string | null @@ -18,11 +20,11 @@ export type TypebotPageProps = { const sessionStorageKey = 'resultId' export const TypebotPage = ({ - typebot, + publishedTypebot, isIE, url, customHeadCode, -}: TypebotPageProps & { typebot: PublicTypebot }) => { +}: TypebotPageProps) => { const { asPath, push } = useRouter() const [showTypebot, setShowTypebot] = useState(false) const [predefinedVariables, setPredefinedVariables] = useState<{ @@ -56,7 +58,7 @@ export const TypebotPage = ({ const hasQueryParams = asPath.includes('?') if ( hasQueryParams && - typebot.settings.general.isHideQueryParamsEnabled !== false + publishedTypebot.settings.general.isHideQueryParamsEnabled !== false ) push(asPath.split('?')[0], undefined, { shallow: true }) } @@ -65,13 +67,15 @@ export const TypebotPage = ({ const resultIdFromSession = getExistingResultFromSession() if (resultIdFromSession) setResultId(resultIdFromSession) else { - const { error, data } = await createResult(typebot.typebotId) + const { error, data } = await createResult(publishedTypebot.typebotId) if (error) return setError(error) if (data?.hasReachedLimit) return setError(new Error('This bot is now closed.')) if (data?.result) { setResultId(data.result.id) - if (typebot.settings.general.isNewResultOnRefreshEnabled !== true) + if ( + publishedTypebot.settings.general.isNewResultOnRefreshEnabled !== true + ) setResultInSession(data.result.id) } } @@ -122,12 +126,12 @@ export const TypebotPage = ({
{showTypebot && ( { - let typebot: Omit | null const isIE = /MSIE|Trident/.test(context.req.headers['user-agent'] ?? '') const pathname = context.resolvedUrl.split('?')[0] const { host, forwardedHost } = getHost(context.req) @@ -31,19 +30,19 @@ export const getServerSideProps: GetServerSideProps = async ( const customDomain = `${forwardedHost ?? host}${ pathname === '/' ? '' : pathname }` - typebot = isMatchingViewerUrl + const publishedTypebot = isMatchingViewerUrl ? await getTypebotFromPublicId(context.query.publicId?.toString()) : await getTypebotFromCustomDomain(customDomain) - if (!typebot) + if (!publishedTypebot) console.log( isMatchingViewerUrl ? `Couldn't find publicId: ${context.query.publicId?.toString()}` : `Couldn't find customDomain: ${customDomain}` ) - const headCode = typebot?.settings.metadata.customHeadCode + const headCode = publishedTypebot?.settings.metadata.customHeadCode return { props: { - typebot, + publishedTypebot, isIE, url: `https://${forwardedHost ?? host}${pathname}`, customHeadCode: @@ -63,23 +62,39 @@ export const getServerSideProps: GetServerSideProps = async ( } } -const getTypebotFromPublicId = async (publicId?: string) => { +const getTypebotFromPublicId = async ( + publicId?: string +): Promise => { if (!publicId) return null - const typebot = await prisma.publicTypebot.findFirst({ + const publishedTypebot = await prisma.publicTypebot.findFirst({ where: { typebot: { publicId } }, - include: { typebot: { select: { name: true } } }, + include: { + typebot: { select: { name: true, isClosed: true, isArchived: true } }, + }, }) - if (isNotDefined(typebot)) return null - return omit(typebot as unknown as PublicTypebot, 'createdAt', 'updatedAt') + if (isNotDefined(publishedTypebot)) return null + return omit( + publishedTypebot, + 'createdAt', + 'updatedAt' + ) as TypebotPageProps['publishedTypebot'] } -const getTypebotFromCustomDomain = async (customDomain: string) => { - const typebot = await prisma.publicTypebot.findFirst({ +const getTypebotFromCustomDomain = async ( + customDomain: string +): Promise => { + const publishedTypebot = await prisma.publicTypebot.findFirst({ where: { typebot: { customDomain } }, - include: { typebot: { select: { name: true } } }, + include: { + typebot: { select: { name: true, isClosed: true, isArchived: true } }, + }, }) - if (isNotDefined(typebot)) return null - return omit(typebot as unknown as PublicTypebot, 'createdAt', 'updatedAt') + if (isNotDefined(publishedTypebot)) return null + return omit( + publishedTypebot, + 'createdAt', + 'updatedAt' + ) as TypebotPageProps['publishedTypebot'] } const getHost = ( @@ -89,11 +104,12 @@ const getHost = ( forwardedHost: req?.headers['x-forwarded-host'] as string | undefined, }) -const App = ({ typebot, ...props }: TypebotPageProps) => - isDefined(typebot) ? ( - - ) : ( - - ) +const App = ({ publishedTypebot, ...props }: TypebotPageProps) => { + if (!publishedTypebot || publishedTypebot.typebot.isArchived) + return + if (publishedTypebot.typebot.isClosed) + return + return +} export default App diff --git a/apps/viewer/playwright/global-setup.ts b/apps/viewer/playwright/global-setup.ts index 4e195e1e083..bf739f4bd45 100644 --- a/apps/viewer/playwright/global-setup.ts +++ b/apps/viewer/playwright/global-setup.ts @@ -1,5 +1,5 @@ import { FullConfig } from '@playwright/test' -import { setupDatabase, teardownDatabase } from './services/database' +import { setupDatabase, teardownDatabase } from 'utils/playwright/databaseSetup' async function globalSetup(config: FullConfig) { const { baseURL } = config.projects[0].use diff --git a/apps/viewer/playwright/services/browser.ts b/apps/viewer/playwright/services/browser.ts deleted file mode 100644 index 9554ac7cf15..00000000000 --- a/apps/viewer/playwright/services/browser.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Page } from '@playwright/test' - -export const mockSessionResponsesToOtherUser = async (page: Page) => - page.route('/api/auth/session', (route) => { - if (route.request().method() === 'GET') { - return route.fulfill({ - status: 200, - 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() - }) diff --git a/apps/viewer/playwright/services/database.ts b/apps/viewer/playwright/services/database.ts deleted file mode 100644 index cbf3ab28ce4..00000000000 --- a/apps/viewer/playwright/services/database.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { - CredentialsType, - defaultSettings, - defaultTheme, - PublicTypebot, - SmtpCredentialsData, - Block, - Typebot, - Webhook, -} from 'models' -import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db' -import { readFileSync } from 'fs' -import { injectFakeResults } from 'utils' -import { encrypt } from 'utils/api' - -const prisma = new PrismaClient() - -const userId = 'userId' -export const freeWorkspaceId = 'freeWorkspace' -export const starterWorkspaceId = 'starterWorkspace' -export const limitTestWorkspaceId = 'limitTestWorkspace' -export const apiToken = 'jirowjgrwGREHE' - -export const teardownDatabase = async () => { - await prisma.workspace.deleteMany({ - where: { - members: { - some: { userId }, - }, - }, - }) - await prisma.user.deleteMany({ - where: { id: userId }, - }) - return prisma.webhook.deleteMany() -} - -export const setupDatabase = async () => { - await createWorkspaces() - await createUser() -} - -export const createWorkspaces = async () => - prisma.workspace.createMany({ - data: [ - { - id: freeWorkspaceId, - name: 'Free workspace', - plan: Plan.FREE, - }, - { - id: starterWorkspaceId, - name: 'Starter workspace', - plan: Plan.STARTER, - }, - { - id: limitTestWorkspaceId, - name: 'Limit test workspace', - plan: Plan.FREE, - }, - ], - }) - -export const createUser = async () => { - await prisma.user.create({ - data: { - id: userId, - email: 'user@email.com', - name: 'John Doe', - graphNavigation: GraphNavigation.TRACKPAD, - apiTokens: { - createMany: { - data: [ - { - name: 'Token', - token: apiToken, - createdAt: new Date(2022, 1, 1), - }, - ], - }, - }, - }, - }) - await prisma.memberInWorkspace.createMany({ - data: [ - { role: WorkspaceRole.ADMIN, userId, workspaceId: freeWorkspaceId }, - { role: WorkspaceRole.ADMIN, userId, workspaceId: starterWorkspaceId }, - { role: WorkspaceRole.ADMIN, userId, workspaceId: limitTestWorkspaceId }, - ], - }) -} - -export const createWebhook = (typebotId: string, webhook?: Partial) => - prisma.webhook.create({ - data: { - id: 'webhook1', - typebotId, - method: 'GET', - ...webhook, - }, - }) - -export const createTypebots = async (partialTypebots: Partial[]) => { - await prisma.typebot.createMany({ - data: partialTypebots.map(parseTestTypebot), - }) - return prisma.publicTypebot.createMany({ - data: partialTypebots.map((t) => - parseTypebotToPublicTypebot(t.id + '-published', parseTestTypebot(t)) - ), - }) -} - -export const updateTypebot = async ( - partialTypebot: Partial & { id: string } -) => { - await prisma.typebot.updateMany({ - where: { id: partialTypebot.id }, - data: partialTypebot, - }) - return prisma.publicTypebot.updateMany({ - where: { typebotId: partialTypebot.id }, - data: partialTypebot, - }) -} - -const parseTypebotToPublicTypebot = ( - id: string, - typebot: Typebot -): PublicTypebot => ({ - id, - groups: typebot.groups, - typebotId: typebot.id, - theme: typebot.theme, - settings: typebot.settings, - variables: typebot.variables, - edges: typebot.edges, - createdAt: typebot.createdAt, - updatedAt: typebot.updatedAt, -}) - -const parseTestTypebot = (partialTypebot: Partial): Typebot => ({ - id: partialTypebot.id ?? 'typebot', - folderId: null, - name: 'My typebot', - workspaceId: freeWorkspaceId, - icon: null, - theme: defaultTheme, - settings: defaultSettings, - publicId: partialTypebot.id + '-public', - publishedTypebotId: null, - updatedAt: new Date().toISOString(), - createdAt: new Date().toISOString(), - customDomain: null, - variables: [{ id: 'var1', name: 'var1' }], - ...partialTypebot, - edges: [ - { - id: 'edge1', - from: { groupId: 'group0', blockId: 'block0' }, - to: { groupId: 'group1' }, - }, - ], - groups: [ - { - id: 'group0', - title: 'Group #0', - blocks: [ - { - id: 'block0', - type: 'start', - groupId: 'group0', - label: 'Start', - outgoingEdgeId: 'edge1', - }, - ], - graphCoordinates: { x: 0, y: 0 }, - }, - ...(partialTypebot.groups ?? []), - ], -}) - -export const parseDefaultGroupWithBlock = ( - block: Partial -): Pick => ({ - groups: [ - { - graphCoordinates: { x: 200, y: 200 }, - id: 'group1', - blocks: [ - { - id: 'block1', - groupId: 'group1', - ...block, - } as Block, - ], - title: 'Group #1', - }, - ], -}) - -export const importTypebotInDatabase = async ( - path: string, - updates?: Partial -) => { - const typebot: Typebot = { - ...JSON.parse(readFileSync(path).toString()), - workspaceId: starterWorkspaceId, - ...updates, - } - await prisma.typebot.create({ - data: typebot, - }) - return prisma.publicTypebot.create({ - data: parseTypebotToPublicTypebot( - updates?.id ? `${updates?.id}-public` : 'publicBot', - typebot - ), - }) -} - -export const createResults = injectFakeResults(prisma) - -export const createSmtpCredentials = ( - id: string, - smtpData: SmtpCredentialsData -) => { - const { encryptedData, iv } = encrypt(smtpData) - return prisma.credentials.create({ - data: { - id, - data: encryptedData, - iv, - name: smtpData.from.email as string, - type: CredentialsType.SMTP, - workspaceId: freeWorkspaceId, - }, - }) -} diff --git a/apps/viewer/playwright/services/databaseActions.ts b/apps/viewer/playwright/services/databaseActions.ts new file mode 100644 index 00000000000..b9f59dc6123 --- /dev/null +++ b/apps/viewer/playwright/services/databaseActions.ts @@ -0,0 +1,23 @@ +import { CredentialsType, SmtpCredentialsData } from 'models' +import { PrismaClient } from 'db' +import { encrypt } from 'utils/api' +import { freeWorkspaceId } from 'utils/playwright/databaseSetup' + +const prisma = new PrismaClient() + +export const createSmtpCredentials = ( + id: string, + smtpData: SmtpCredentialsData +) => { + const { encryptedData, iv } = encrypt(smtpData) + return prisma.credentials.create({ + data: { + id, + data: encryptedData, + iv, + name: smtpData.from.email as string, + type: CredentialsType.SMTP, + workspaceId: freeWorkspaceId, + }, + }) +} diff --git a/apps/viewer/playwright/services/selectorUtils.ts b/apps/viewer/playwright/services/selectorUtils.ts deleted file mode 100644 index a4bac467c9b..00000000000 --- a/apps/viewer/playwright/services/selectorUtils.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Page } from '@playwright/test' - -export const typebotViewer = (page: Page) => - page.frameLocator('#typebot-iframe') diff --git a/apps/viewer/playwright/tests/api.spec.ts b/apps/viewer/playwright/tests/api.spec.ts index 10bae4ff750..c2f449187fc 100644 --- a/apps/viewer/playwright/tests/api.spec.ts +++ b/apps/viewer/playwright/tests/api.spec.ts @@ -1,11 +1,11 @@ import test, { expect } from '@playwright/test' +import path from 'path' import { - apiToken, - createResults, - createWebhook, importTypebotInDatabase, -} from '../services/database' -import path from 'path' + createWebhook, + injectFakeResults, +} from 'utils/playwright/databaseActions' +import { apiToken } from 'utils/playwright/databaseSetup' const typebotId = 'webhook-flow' test.beforeAll(async () => { @@ -15,7 +15,7 @@ test.beforeAll(async () => { { id: typebotId } ) await createWebhook(typebotId) - await createResults({ typebotId, count: 20 }) + await injectFakeResults({ typebotId, count: 20 }) } catch (err) { console.log(err) } diff --git a/apps/viewer/playwright/tests/fileUpload.spec.ts b/apps/viewer/playwright/tests/fileUpload.spec.ts index f1466641d31..6de7c219d0e 100644 --- a/apps/viewer/playwright/tests/fileUpload.spec.ts +++ b/apps/viewer/playwright/tests/fileUpload.spec.ts @@ -2,10 +2,10 @@ import test, { expect } from '@playwright/test' import cuid from 'cuid' import path from 'path' import { parse } from 'papaparse' -import { typebotViewer } from '../services/selectorUtils' -import { createResults, importTypebotInDatabase } from '../services/database' import { readFileSync } from 'fs' import { isDefined } from 'utils' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024 diff --git a/apps/viewer/playwright/tests/hugeBlock.spec.ts b/apps/viewer/playwright/tests/hugeBlock.spec.ts index 36491f7801e..4739d39ba17 100644 --- a/apps/viewer/playwright/tests/hugeBlock.spec.ts +++ b/apps/viewer/playwright/tests/hugeBlock.spec.ts @@ -1,8 +1,8 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { importTypebotInDatabase } from '../services/database' -import { typebotViewer } from '../services/selectorUtils' import cuid from 'cuid' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' test('should work as expected', async ({ page }) => { const typebotId = cuid() diff --git a/apps/viewer/playwright/tests/limits.spec.ts b/apps/viewer/playwright/tests/limits.spec.ts index 7592c841167..edd75c26132 100644 --- a/apps/viewer/playwright/tests/limits.spec.ts +++ b/apps/viewer/playwright/tests/limits.spec.ts @@ -1,9 +1,4 @@ import test, { expect } from '@playwright/test' -import { - createResults, - importTypebotInDatabase, - limitTestWorkspaceId, -} from '../services/database' import cuid from 'cuid' import path from 'path' diff --git a/apps/viewer/playwright/tests/metadata.spec.ts b/apps/viewer/playwright/tests/metadata.spec.ts index d2daee8015f..1ee077e6791 100644 --- a/apps/viewer/playwright/tests/metadata.spec.ts +++ b/apps/viewer/playwright/tests/metadata.spec.ts @@ -1,16 +1,14 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, -} from '../services/database' import { defaultSettings, defaultTextInputOptions, InputBlockType, Metadata, } from 'models' -import { typebotViewer } from '../services/selectorUtils' import cuid from 'cuid' +import { createTypebots } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' +import { typebotViewer } from 'utils/playwright/testHelpers' test('Should correctly parse metadata', async ({ page }) => { const typebotId = cuid() diff --git a/apps/viewer/playwright/tests/predefinedVariables.spec.ts b/apps/viewer/playwright/tests/predefinedVariables.spec.ts index 7c70c3fa835..7434e646446 100644 --- a/apps/viewer/playwright/tests/predefinedVariables.spec.ts +++ b/apps/viewer/playwright/tests/predefinedVariables.spec.ts @@ -1,8 +1,8 @@ import test, { expect } from '@playwright/test' -import { importTypebotInDatabase } from '../services/database' import cuid from 'cuid' import path from 'path' -import { typebotViewer } from '../services/selectorUtils' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' test('should correctly be injected', async ({ page }) => { const typebotId = cuid() diff --git a/apps/viewer/playwright/tests/sendEmail.spec.ts b/apps/viewer/playwright/tests/sendEmail.spec.ts index cb37684bebe..9f79ce3c9a0 100644 --- a/apps/viewer/playwright/tests/sendEmail.spec.ts +++ b/apps/viewer/playwright/tests/sendEmail.spec.ts @@ -1,12 +1,10 @@ import test, { expect } from '@playwright/test' -import { - createSmtpCredentials, - importTypebotInDatabase, -} from '../services/database' +import { createSmtpCredentials } from '../services/databaseActions' import cuid from 'cuid' import path from 'path' -import { typebotViewer } from '../services/selectorUtils' import { SmtpCredentialsData } from 'models' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' const mockSmtpCredentials: SmtpCredentialsData = { from: { diff --git a/apps/viewer/playwright/tests/settings.spec.ts b/apps/viewer/playwright/tests/settings.spec.ts index 98b18c0c65d..f1c0f73f942 100644 --- a/apps/viewer/playwright/tests/settings.spec.ts +++ b/apps/viewer/playwright/tests/settings.spec.ts @@ -1,15 +1,12 @@ import test, { expect } from '@playwright/test' -import { - createTypebots, - parseDefaultGroupWithBlock, - updateTypebot, -} from '../services/database' import cuid from 'cuid' import { defaultSettings, defaultTextInputOptions, InputBlockType, } from 'models' +import { createTypebots, updateTypebot } from 'utils/playwright/databaseActions' +import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers' test('Result should be in storage by default', async ({ page }) => { const typebotId = cuid() @@ -107,3 +104,19 @@ test('Hide query params', async ({ page }) => { `http://localhost:3001/${typebotId}-public?Name=John` ) }) + +test('Show close message', async ({ page }) => { + const typebotId = cuid() + await createTypebots([ + { + id: typebotId, + ...parseDefaultGroupWithBlock({ + type: InputBlockType.TEXT, + options: defaultTextInputOptions, + }), + isClosed: true, + }, + ]) + await page.goto(`/${typebotId}-public`) + await expect(page.locator('text=This bot is now closed')).toBeVisible() +}) diff --git a/apps/viewer/playwright/tests/typebotLink.spec.ts b/apps/viewer/playwright/tests/typebotLink.spec.ts index b57d634d86d..4b30f7d35f7 100644 --- a/apps/viewer/playwright/tests/typebotLink.spec.ts +++ b/apps/viewer/playwright/tests/typebotLink.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from '@playwright/test' import path from 'path' -import { importTypebotInDatabase } from '../services/database' -import { typebotViewer } from '../services/selectorUtils' +import { importTypebotInDatabase } from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' test('should work as expected', async ({ page }) => { const typebotId = 'cl0ibhi7s0018n21aarlmg0cm' diff --git a/apps/viewer/playwright/tests/webhook.spec.ts b/apps/viewer/playwright/tests/webhook.spec.ts index 3223394d46b..0de8f26e8bc 100644 --- a/apps/viewer/playwright/tests/webhook.spec.ts +++ b/apps/viewer/playwright/tests/webhook.spec.ts @@ -1,9 +1,12 @@ import test, { expect } from '@playwright/test' -import { createWebhook, importTypebotInDatabase } from '../services/database' import cuid from 'cuid' import path from 'path' -import { typebotViewer } from '../services/selectorUtils' import { HttpMethod } from 'models' +import { + createWebhook, + importTypebotInDatabase, +} from 'utils/playwright/databaseActions' +import { typebotViewer } from 'utils/playwright/testHelpers' test('should execute webhooks properly', async ({ page }) => { const typebotId = cuid() diff --git a/packages/bot-engine/src/components/TypebotViewer.tsx b/packages/bot-engine/src/components/TypebotViewer.tsx index f895afb9a73..9569c80bc3a 100644 --- a/packages/bot-engine/src/components/TypebotViewer.tsx +++ b/packages/bot-engine/src/components/TypebotViewer.tsx @@ -24,7 +24,7 @@ import { LiteBadge } from './LiteBadge' import { getViewerUrl, isEmpty } from 'utils' export type TypebotViewerProps = { - typebot: PublicTypebot + typebot: Omit isPreview?: boolean apiHost?: string style?: CSSProperties diff --git a/packages/bot-engine/src/contexts/TypebotContext.tsx b/packages/bot-engine/src/contexts/TypebotContext.tsx index 764eec650ba..3446794f755 100644 --- a/packages/bot-engine/src/contexts/TypebotContext.tsx +++ b/packages/bot-engine/src/contexts/TypebotContext.tsx @@ -1,3 +1,4 @@ +import { TypebotViewerProps } from 'components/TypebotViewer' import { Log } from 'db' import { Edge, PublicTypebot, Typebot } from 'models' import React, { @@ -20,7 +21,7 @@ export type LinkedTypebotQueue = { const typebotContext = createContext<{ currentTypebotId: string - typebot: PublicTypebot + typebot: TypebotViewerProps['typebot'] linkedTypebots: LinkedTypebot[] apiHost: string isPreview: boolean @@ -49,13 +50,14 @@ export const TypebotContext = ({ onNewLog, }: { children: ReactNode - typebot: PublicTypebot + typebot: TypebotViewerProps['typebot'] apiHost: string isLoading: boolean isPreview: boolean onNewLog: (log: Omit) => void }) => { - const [localTypebot, setLocalTypebot] = useState(typebot) + const [localTypebot, setLocalTypebot] = + useState(typebot) const [linkedTypebots, setLinkedTypebots] = useState([]) const [currentTypebotId, setCurrentTypebotId] = useState(typebot.typebotId) const [linkedBotQueue, setLinkedBotQueue] = useState([]) diff --git a/packages/bot-engine/src/services/logic.ts b/packages/bot-engine/src/services/logic.ts index c359b5d3414..3c9062c9710 100644 --- a/packages/bot-engine/src/services/logic.ts +++ b/packages/bot-engine/src/services/logic.ts @@ -1,3 +1,4 @@ +import { TypebotViewerProps } from 'components/TypebotViewer' import { LinkedTypebot } from 'contexts/TypebotContext' import { Log } from 'db' import { @@ -26,7 +27,7 @@ type EdgeId = string type LogicContext = { isPreview: boolean apiHost: string - typebot: PublicTypebot + typebot: TypebotViewerProps['typebot'] linkedTypebots: LinkedTypebot[] currentTypebotId: string pushEdgeIdInLinkedTypebotQueue: (bot: { @@ -46,7 +47,7 @@ export const executeLogic = async ( context: LogicContext ): Promise<{ nextEdgeId?: EdgeId - linkedTypebot?: PublicTypebot | LinkedTypebot + linkedTypebot?: TypebotViewerProps['typebot'] | LinkedTypebot }> => { switch (block.type) { case LogicBlockType.SET_VARIABLE: diff --git a/packages/db/prisma/migrations/20221006063227_add_is_closed_to_typebot/migration.sql b/packages/db/prisma/migrations/20221006063227_add_is_closed_to_typebot/migration.sql new file mode 100644 index 00000000000..5652dfcc05b --- /dev/null +++ b/packages/db/prisma/migrations/20221006063227_add_is_closed_to_typebot/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Typebot" ADD COLUMN "isClosed" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 425325ea0a2..1405b4814d9 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -168,7 +168,8 @@ model Typebot { publishedTypebot PublicTypebot? results Result[] webhooks Webhook[] - isArchived Boolean @default(false) + isArchived Boolean @default(false) + isClosed Boolean @default(false) } model Invitation { diff --git a/packages/models/typebot/typebot.ts b/packages/models/typebot/typebot.ts index 8e508bac02c..775301c06bf 100644 --- a/packages/models/typebot/typebot.ts +++ b/packages/models/typebot/typebot.ts @@ -55,6 +55,8 @@ const typebotSchema = z.object({ customDomain: z.string().nullable(), workspaceId: z.string(), resultsTablePreferences: resultsTablePreferencesSchema.optional(), + isArchived: z.boolean(), + isClosed: z.boolean(), }) export type Typebot = z.infer diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 776507bacf6..6fd680db16a 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,4 +1,3 @@ export * from './utils' export * from './results' export * from './pricing' -export * from './playwright' diff --git a/packages/utils/package.json b/packages/utils/package.json index 86a708db83e..9d4a9abb620 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -6,6 +6,7 @@ "main": "./index.ts", "types": "./index.ts", "devDependencies": { + "@playwright/test": "1.26.1", "@types/nodemailer": "6.4.6", "aws-sdk": "2.1227.0", "cuid": "2.1.8", diff --git a/packages/utils/playwright.ts b/packages/utils/playwright.ts deleted file mode 100644 index 3b7924fc694..00000000000 --- a/packages/utils/playwright.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { PrismaClient } from 'db' -import cuid from 'cuid' - -type CreateFakeResultsProps = { - typebotId: string - count: number - customResultIdPrefix?: string - isChronological?: boolean - fakeStorage?: number -} - -export const injectFakeResults = - (prisma: PrismaClient) => - async ({ - count, - customResultIdPrefix, - typebotId, - isChronological, - fakeStorage, - }: CreateFakeResultsProps) => { - const resultIdPrefix = customResultIdPrefix ?? cuid() - await prisma.result.createMany({ - data: [ - ...Array.from(Array(count)).map((_, idx) => { - const today = new Date() - const rand = Math.random() - return { - id: `${resultIdPrefix}-result${idx}`, - typebotId, - createdAt: isChronological - ? new Date( - today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx) - ) - : new Date(), - isCompleted: rand > 0.5, - hasStarted: true, - } - }), - ], - }) - return createAnswers(prisma)({ fakeStorage, resultIdPrefix, count }) - } - -const createAnswers = - (prisma: PrismaClient) => - ({ - count, - resultIdPrefix, - fakeStorage, - }: { resultIdPrefix: string } & Pick< - CreateFakeResultsProps, - 'fakeStorage' | 'count' - >) => { - return prisma.answer.createMany({ - data: [ - ...Array.from(Array(count)).map((_, idx) => ({ - resultId: `${resultIdPrefix}-result${idx}`, - content: `content${idx}`, - blockId: 'block1', - groupId: 'block1', - storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null, - })), - ], - }) - } diff --git a/packages/utils/playwright/databaseActions.ts b/packages/utils/playwright/databaseActions.ts new file mode 100644 index 00000000000..5666bbb71d2 --- /dev/null +++ b/packages/utils/playwright/databaseActions.ts @@ -0,0 +1,168 @@ +import { Plan, PrismaClient, User, Workspace, WorkspaceRole } from 'db' +import cuid from 'cuid' +import { Typebot, Webhook } from 'models' +import { readFileSync } from 'fs' +import { proWorkspaceId, userId } from './databaseSetup' +import { + parseTestTypebot, + parseTypebotToPublicTypebot, +} from './databaseHelpers' + +const prisma = new PrismaClient() + +type CreateFakeResultsProps = { + typebotId: string + count: number + customResultIdPrefix?: string + isChronological?: boolean + fakeStorage?: number +} + +export const injectFakeResults = async ({ + count, + customResultIdPrefix, + typebotId, + isChronological, + fakeStorage, +}: CreateFakeResultsProps) => { + const resultIdPrefix = customResultIdPrefix ?? cuid() + await prisma.result.createMany({ + data: [ + ...Array.from(Array(count)).map((_, idx) => { + const today = new Date() + const rand = Math.random() + return { + id: `${resultIdPrefix}-result${idx}`, + typebotId, + createdAt: isChronological + ? new Date( + today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx) + ) + : new Date(), + isCompleted: rand > 0.5, + hasStarted: true, + } + }), + ], + }) + return createAnswers({ fakeStorage, resultIdPrefix, count }) +} + +const createAnswers = ({ + count, + resultIdPrefix, + fakeStorage, +}: { resultIdPrefix: string } & Pick< + CreateFakeResultsProps, + 'fakeStorage' | 'count' +>) => { + return prisma.answer.createMany({ + data: [ + ...Array.from(Array(count)).map((_, idx) => ({ + resultId: `${resultIdPrefix}-result${idx}`, + content: `content${idx}`, + blockId: 'block1', + groupId: 'block1', + storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null, + })), + ], + }) +} + +export const importTypebotInDatabase = async ( + path: string, + updates?: Partial +) => { + const typebot: Typebot = { + ...JSON.parse(readFileSync(path).toString()), + workspaceId: proWorkspaceId, + ...updates, + } + await prisma.typebot.create({ + data: typebot, + }) + return prisma.publicTypebot.create({ + data: parseTypebotToPublicTypebot( + updates?.id ? `${updates?.id}-public` : 'publicBot', + typebot + ), + }) +} + +export const deleteWorkspaces = async (workspaceIds: string[]) => { + await prisma.workspace.deleteMany({ + where: { id: { in: workspaceIds } }, + }) +} + +export const createWorkspaces = async (workspaces: Partial[]) => { + const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid()) + await prisma.workspace.createMany({ + data: workspaces.map((workspace, index) => ({ + id: workspaceIds[index], + name: 'Free workspace', + plan: Plan.FREE, + ...workspace, + })), + }) + await prisma.memberInWorkspace.createMany({ + data: workspaces.map((_, index) => ({ + userId, + workspaceId: workspaceIds[index], + role: WorkspaceRole.ADMIN, + })), + }) + return workspaceIds +} + +export const updateUser = (data: Partial) => + prisma.user.update({ + data, + where: { + id: userId, + }, + }) + +export const createWebhook = async ( + typebotId: string, + webhookProps?: Partial +) => { + try { + await prisma.webhook.delete({ where: { id: 'webhook1' } }) + } catch {} + return prisma.webhook.create({ + data: { method: 'GET', typebotId, id: 'webhook1', ...webhookProps }, + }) +} + +export const createTypebots = async (partialTypebots: Partial[]) => { + const typebotsWithId = partialTypebots.map((typebot) => { + const typebotId = typebot.id ?? cuid() + return { + ...typebot, + id: typebotId, + publicId: typebotId + '-public', + } + }) + await prisma.typebot.createMany({ + data: typebotsWithId.map(parseTestTypebot), + }) + return prisma.publicTypebot.createMany({ + data: typebotsWithId.map((t) => + parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t)) + ), + }) +} + +export const updateTypebot = async ( + partialTypebot: Partial & { id: string } +) => { + await prisma.typebot.updateMany({ + where: { id: partialTypebot.id }, + data: partialTypebot, + }) + return prisma.publicTypebot.updateMany({ + where: { typebotId: partialTypebot.id }, + data: partialTypebot, + }) +} diff --git a/packages/utils/playwright/databaseHelpers.ts b/packages/utils/playwright/databaseHelpers.ts new file mode 100644 index 00000000000..9d973a5e256 --- /dev/null +++ b/packages/utils/playwright/databaseHelpers.ts @@ -0,0 +1,86 @@ +import cuid from 'cuid' +import { + Block, + defaultSettings, + defaultTheme, + PublicTypebot, + Typebot, +} from 'models' +import { proWorkspaceId } from './databaseSetup' + +export const parseTestTypebot = ( + partialTypebot: Partial +): Typebot => ({ + id: cuid(), + workspaceId: proWorkspaceId, + folderId: null, + name: 'My typebot', + theme: defaultTheme, + settings: defaultSettings, + publicId: null, + updatedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + publishedTypebotId: null, + customDomain: null, + icon: null, + isArchived: false, + isClosed: false, + variables: [{ id: 'var1', name: 'var1' }], + ...partialTypebot, + edges: [ + { + id: 'edge1', + from: { groupId: 'block0', blockId: 'block0' }, + to: { groupId: 'block1' }, + }, + ], + groups: [ + { + id: 'block0', + title: 'Group #0', + blocks: [ + { + id: 'block0', + type: 'start', + groupId: 'block0', + label: 'Start', + outgoingEdgeId: 'edge1', + }, + ], + graphCoordinates: { x: 0, y: 0 }, + }, + ...(partialTypebot.groups ?? []), + ], +}) + +export const parseTypebotToPublicTypebot = ( + id: string, + typebot: Typebot +): Omit => ({ + id, + groups: typebot.groups, + typebotId: typebot.id, + theme: typebot.theme, + settings: typebot.settings, + variables: typebot.variables, + edges: typebot.edges, +}) + +export const parseDefaultGroupWithBlock = ( + block: Partial +): Pick => ({ + groups: [ + { + graphCoordinates: { x: 200, y: 200 }, + id: 'block1', + blocks: [ + { + id: 'block1', + groupId: 'block1', + ...block, + } as Block, + ], + title: 'Group #1', + }, + ], +}) diff --git a/packages/utils/playwright/databaseSetup.ts b/packages/utils/playwright/databaseSetup.ts new file mode 100644 index 00000000000..025ab26c983 --- /dev/null +++ b/packages/utils/playwright/databaseSetup.ts @@ -0,0 +1,146 @@ +import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db' +import { CredentialsType } from 'models' +import { encrypt } from '../api' + +const prisma = new PrismaClient() + +export const apiToken = 'jirowjgrwGREHE' + +export const userId = 'userId' +export const otherUserId = 'otherUserId' + +export const proWorkspaceId = 'proWorkspace' +export const freeWorkspaceId = 'freeWorkspace' +export const starterWorkspaceId = 'starterWorkspace' +export const lifetimeWorkspaceId = 'lifetimeWorkspaceId' + +const setupWorkspaces = async () => { + await prisma.workspace.create({ + data: { + id: freeWorkspaceId, + name: 'Free workspace', + plan: Plan.FREE, + }, + }) + await prisma.workspace.createMany({ + data: [ + { + id: starterWorkspaceId, + name: 'Starter workspace', + stripeId: 'cus_LnPDugJfa18N41', + plan: Plan.STARTER, + }, + { + id: proWorkspaceId, + name: 'Pro workspace', + plan: Plan.PRO, + }, + { + id: lifetimeWorkspaceId, + name: 'Lifetime workspace', + plan: Plan.LIFETIME, + }, + ], + }) +} + +export const setupUsers = async () => { + await prisma.user.create({ + data: { + id: userId, + email: 'user@email.com', + name: 'John Doe', + graphNavigation: GraphNavigation.TRACKPAD, + apiTokens: { + createMany: { + data: [ + { + name: 'Token 1', + token: apiToken, + 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), + }, + ], + }, + }, + }, + }) + await prisma.user.create({ + data: { id: otherUserId, email: 'other-user@email.com', name: 'James Doe' }, + }) + return prisma.memberInWorkspace.createMany({ + data: [ + { + role: WorkspaceRole.ADMIN, + userId, + workspaceId: freeWorkspaceId, + }, + { + role: WorkspaceRole.ADMIN, + userId, + workspaceId: starterWorkspaceId, + }, + { + role: WorkspaceRole.ADMIN, + userId, + workspaceId: proWorkspaceId, + }, + { + role: WorkspaceRole.ADMIN, + userId, + workspaceId: lifetimeWorkspaceId, + }, + ], + }) +} + +const setupCredentials = () => { + const { encryptedData, iv } = encrypt({ + expiry_date: 1642441058842, + access_token: + 'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod', + // This token is linked to a test Google account (typebot.test.user@gmail.com) + refresh_token: + '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek', + }) + return prisma.credentials.createMany({ + data: [ + { + name: 'pro-user@email.com', + type: CredentialsType.GOOGLE_SHEETS, + data: encryptedData, + workspaceId: proWorkspaceId, + iv, + }, + ], + }) +} + +export const setupDatabase = async () => { + await setupWorkspaces() + await setupUsers() + return setupCredentials() +} + +export const teardownDatabase = async () => { + await prisma.workspace.deleteMany({ + where: { + members: { + some: { userId: { in: [userId, otherUserId] } }, + }, + }, + }) + await prisma.user.deleteMany({ + where: { id: { in: [userId, otherUserId] } }, + }) + return prisma.webhook.deleteMany() +} diff --git a/packages/utils/playwright/testHelpers.ts b/packages/utils/playwright/testHelpers.ts new file mode 100644 index 00000000000..ad2b79c5f71 --- /dev/null +++ b/packages/utils/playwright/testHelpers.ts @@ -0,0 +1,30 @@ +import type { Page } from '@playwright/test' + +export const mockSessionResponsesToOtherUser = async (page: Page) => + page.route('/api/auth/session', (route) => { + if (route.request().method() === 'GET') { + return route.fulfill({ + status: 200, + 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() + }) + +export const typebotViewer = (page: Page) => + page.frameLocator('#typebot-iframe') + +export const waitForSuccessfulPutRequest = (page: Page) => + page.waitForResponse( + (resp) => resp.request().method() === 'PUT' && resp.status() === 200 + ) + +export const waitForSuccessfulPostRequest = (page: Page) => + page.waitForResponse( + (resp) => resp.request().method() === 'POST' && resp.status() === 200 + ) + +export const waitForSuccessfulDeleteRequest = (page: Page) => + page.waitForResponse( + (resp) => resp.request().method() === 'DELETE' && resp.status() === 200 + ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196f3b62de4..cae56da3b52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -339,6 +339,7 @@ importers: db: workspace:* dotenv: 16.0.3 emails: workspace:* + encoding: ^0.1.13 eslint: 8.24.0 eslint-config-next: 12.3.1 eslint-plugin-react: 7.31.8 @@ -361,13 +362,13 @@ importers: uglify-js: 3.17.2 utils: workspace:* dependencies: - '@sentry/nextjs': 7.14.0_next@12.3.1+react@18.2.0 + '@sentry/nextjs': 7.14.0_jxu4po4uwlfmcmtoojj2rjxcii aws-sdk: 2.1227.0 bot-engine: link:../../packages/bot-engine cors: 2.8.5 cuid: 2.1.8 db: link:../../packages/db - google-spreadsheet: 3.3.0 + google-spreadsheet: 3.3.0_encoding@0.1.13 got: 12.5.1 next: 12.3.1_6tziyx3dehkoeijunclpkpolha nodemailer: 6.8.0 @@ -391,11 +392,12 @@ importers: '@typescript-eslint/parser': 5.38.1_ypn2ylkkyfa5i233caldtndbqa dotenv: 16.0.3 emails: link:../../packages/emails + encoding: 0.1.13 eslint: 8.24.0 eslint-config-next: 12.3.1_ypn2ylkkyfa5i233caldtndbqa eslint-plugin-react: 7.31.8_eslint@8.24.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.24.0 - google-auth-library: 8.5.2 + google-auth-library: 8.5.2_encoding@0.1.13 models: link:../../packages/models next-transpile-modules: 9.0.0 node-fetch: 3.2.10 @@ -603,6 +605,7 @@ importers: packages/utils: specifiers: + '@playwright/test': 1.26.1 '@types/nodemailer': 6.4.6 aws-sdk: 2.1227.0 cuid: 2.1.8 @@ -612,6 +615,7 @@ importers: nodemailer: 6.8.0 typescript: 4.8.4 devDependencies: + '@playwright/test': 1.26.1 '@types/nodemailer': 6.4.6 aws-sdk: 2.1227.0 cuid: 2.1.8 @@ -5151,6 +5155,24 @@ packages: - supports-color dev: false + /@sentry/cli/1.74.5_encoding@0.1.13: + resolution: {integrity: sha512-Ze1ec306ZWHtrxKypOJ8nhtFqkrx2f/6bRH+DcJzEQ3bBePQ0ZnqJTTe4BBHADYBtxFIaUWzCZ6DquLz2Zv/sw==} + engines: {node: '>= 8'} + hasBin: true + requiresBuild: true + dependencies: + https-proxy-agent: 5.0.1 + mkdirp: 0.5.6 + node-fetch: 2.6.7_encoding@0.1.13 + npmlog: 4.1.2 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@sentry/core/7.14.0: resolution: {integrity: sha512-Hgn7De6CiCFnz868/Lrtei+9rj7/TIwhbDe3J+NeH+2ffXYn4VI8FxrlR/p2XfIq9iCfmG80EQXDtSh+Kh7mOw==} engines: {node: '>=8'} @@ -5180,6 +5202,37 @@ packages: tslib: 1.14.1 dev: false + /@sentry/nextjs/7.14.0_jxu4po4uwlfmcmtoojj2rjxcii: + resolution: {integrity: sha512-TmBWGuvNmdHbDCsNYiSwcO0WiXpOl9eLdLvCN1pitXO5YWWPUGjKKWVQ54WZCkVF4iiqsnMBNLCtBApU1Hu9qA==} + engines: {node: '>=8'} + peerDependencies: + next: ^10.0.8 || ^11.0 || ^12.0 + react: 15.x || 16.x || 17.x || 18.x + webpack: '>= 4.0.0' + peerDependenciesMeta: + webpack: + optional: true + dependencies: + '@rollup/plugin-sucrase': 4.0.4_rollup@2.78.0 + '@sentry/core': 7.14.0 + '@sentry/hub': 7.14.0 + '@sentry/integrations': 7.14.0 + '@sentry/node': 7.14.0 + '@sentry/react': 7.14.0_react@18.2.0 + '@sentry/tracing': 7.14.0 + '@sentry/types': 7.14.0 + '@sentry/utils': 7.14.0 + '@sentry/webpack-plugin': 1.19.0_encoding@0.1.13 + chalk: 3.0.0 + next: 12.3.1_6tziyx3dehkoeijunclpkpolha + react: 18.2.0 + rollup: 2.78.0 + tslib: 1.14.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@sentry/nextjs/7.14.0_next@12.3.1+react@18.2.0: resolution: {integrity: sha512-TmBWGuvNmdHbDCsNYiSwcO0WiXpOl9eLdLvCN1pitXO5YWWPUGjKKWVQ54WZCkVF4iiqsnMBNLCtBApU1Hu9qA==} engines: {node: '>=8'} @@ -5274,6 +5327,16 @@ packages: - supports-color dev: false + /@sentry/webpack-plugin/1.19.0_encoding@0.1.13: + resolution: {integrity: sha512-qSpdgdGMtdzagGveSWgo2b+t8PdPUscuOjbOyWCsJme9jlTFnNk0rX7JEA55OUozikKHM/+vVh08USLBnPboZw==} + engines: {node: '>= 8'} + dependencies: + '@sentry/cli': 1.74.5_encoding@0.1.13 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@sideway/address/4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -9075,6 +9138,11 @@ packages: engines: {node: '>= 0.8'} dev: false + /encoding/0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + dependencies: + iconv-lite: 0.6.3 + /end-of-stream/1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -10346,6 +10414,20 @@ packages: - supports-color dev: false + /gaxios/4.3.3_encoding@0.1.13: + resolution: {integrity: sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==} + engines: {node: '>=10'} + dependencies: + abort-controller: 3.0.0 + extend: 3.0.2 + https-proxy-agent: 5.0.1 + is-stream: 2.0.1 + node-fetch: 2.6.7_encoding@0.1.13 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gaxios/5.0.2: resolution: {integrity: sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==} engines: {node: '>=12'} @@ -10357,6 +10439,20 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: false + + /gaxios/5.0.2_encoding@0.1.13: + resolution: {integrity: sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==} + engines: {node: '>=12'} + dependencies: + extend: 3.0.2 + https-proxy-agent: 5.0.1 + is-stream: 2.0.1 + node-fetch: 2.6.7_encoding@0.1.13 + transitivePeerDependencies: + - encoding + - supports-color + dev: true /gcp-metadata/4.3.1: resolution: {integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==} @@ -10369,6 +10465,17 @@ packages: - supports-color dev: false + /gcp-metadata/4.3.1_encoding@0.1.13: + resolution: {integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==} + engines: {node: '>=10'} + dependencies: + gaxios: 4.3.3_encoding@0.1.13 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gcp-metadata/5.0.1: resolution: {integrity: sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==} engines: {node: '>=12'} @@ -10378,6 +10485,18 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: false + + /gcp-metadata/5.0.1_encoding@0.1.13: + resolution: {integrity: sha512-jiRJ+Fk7e8FH68Z6TLaqwea307OktJpDjmYnU7/li6ziwvVvU2RlrCyQo5vkdeP94chm0kcSCOOszvmuaioq3g==} + engines: {node: '>=12'} + dependencies: + gaxios: 5.0.2_encoding@0.1.13 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: true /generic-names/4.0.0: resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} @@ -10578,6 +10697,24 @@ packages: - supports-color dev: false + /google-auth-library/6.1.6_encoding@0.1.13: + resolution: {integrity: sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ==} + engines: {node: '>=10'} + dependencies: + arrify: 2.0.1 + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + fast-text-encoding: 1.0.6 + gaxios: 4.3.3_encoding@0.1.13 + gcp-metadata: 4.3.1_encoding@0.1.13 + gtoken: 5.3.2_encoding@0.1.13 + jws: 4.0.0 + lru-cache: 6.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /google-auth-library/7.14.1: resolution: {integrity: sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==} engines: {node: '>=10'} @@ -10612,6 +10749,25 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: false + + /google-auth-library/8.5.2_encoding@0.1.13: + resolution: {integrity: sha512-FPfOSaI8n2TVXFHTP8/vAVFCXhyALj7w9/Rgefux3oeKZ/nQDNmfNTJ+lIKcoYT1cKkvMllp1Eood7Y5L+TP+A==} + engines: {node: '>=12'} + dependencies: + arrify: 2.0.1 + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + fast-text-encoding: 1.0.6 + gaxios: 5.0.2_encoding@0.1.13 + gcp-metadata: 5.0.1_encoding@0.1.13 + gtoken: 6.1.2_encoding@0.1.13 + jws: 4.0.0 + lru-cache: 6.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: true /google-p12-pem/3.1.4: resolution: {integrity: sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==} @@ -10641,6 +10797,19 @@ packages: - supports-color dev: false + /google-spreadsheet/3.3.0_encoding@0.1.13: + resolution: {integrity: sha512-ahmRNh14s1i3phfvbF2mxen1lohWJpUaFWgsU6P6bXu7QrmxMaim1Ys/7BU4W5yucWCzphoIrHMbrbeIR5K9mw==} + engines: {node: '>=0.8.0'} + dependencies: + axios: 0.21.4 + google-auth-library: 6.1.6_encoding@0.1.13 + lodash: 4.17.21 + transitivePeerDependencies: + - debug + - encoding + - supports-color + dev: false + /googleapis-common/5.1.0: resolution: {integrity: sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==} engines: {node: '>=10.10.0'} @@ -10721,6 +10890,18 @@ packages: - supports-color dev: false + /gtoken/5.3.2_encoding@0.1.13: + resolution: {integrity: sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==} + engines: {node: '>=10'} + dependencies: + gaxios: 4.3.3_encoding@0.1.13 + google-p12-pem: 3.1.4 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gtoken/6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} engines: {node: '>=12.0.0'} @@ -10731,6 +10912,19 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: false + + /gtoken/6.1.2_encoding@0.1.13: + resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} + engines: {node: '>=12.0.0'} + dependencies: + gaxios: 5.0.2_encoding@0.1.13 + google-p12-pem: 4.0.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: true /gzip-size/6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} @@ -11148,7 +11342,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - dev: true /icss-replace-symbols/1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -13576,6 +13769,18 @@ packages: dependencies: whatwg-url: 5.0.0 + /node-fetch/2.6.7_encoding@0.1.13: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + encoding: 0.1.13 + whatwg-url: 5.0.0 + /node-fetch/3.2.10: resolution: {integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}