mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 00:49:42 -06:00
fix: e2e tests (#2806)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
58
.github/workflows/e2e.yml
vendored
58
.github/workflows/e2e.yml
vendored
@@ -7,6 +7,20 @@ jobs:
|
||||
name: Run E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U testuser"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -16,44 +30,46 @@ jobs:
|
||||
with:
|
||||
e2e_testing_mode: "1"
|
||||
|
||||
- name: Check if pnpm is installed
|
||||
id: pnpm-check
|
||||
run: |
|
||||
if pnpm --version; then
|
||||
echo "pnpm is installed."
|
||||
echo "PNPM_INSTALLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "pnpm is not installed."
|
||||
echo "PNPM_INSTALLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
run: pnpm install
|
||||
|
||||
- name: Start PostgreSQL
|
||||
- name: Apply Prisma Migrations
|
||||
run: |
|
||||
cd packages/database && pnpm db:up &
|
||||
for attempt in {1..20}; do
|
||||
if nc -zv localhost 5432; then
|
||||
echo "Ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting..."
|
||||
sleep 5
|
||||
done
|
||||
pnpm db:migrate:dev
|
||||
- name: Serve packages for lazy loading
|
||||
run: |
|
||||
cd packages/surveys && pnpm serve &
|
||||
pnpm prisma migrate deploy
|
||||
|
||||
- name: Run App
|
||||
run: |
|
||||
NODE_ENV=test pnpm start --filter=@formbricks/web &
|
||||
for attempt in {1..20}; do
|
||||
sleep 10 # Optional: gives some buffer for the app to start
|
||||
for attempt in {1..10}; do
|
||||
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
|
||||
echo "Ready"
|
||||
echo "Application is ready."
|
||||
break
|
||||
fi
|
||||
echo "Waiting..."
|
||||
if [ $attempt -eq 10 ]; then
|
||||
echo "Application failed to start in time."
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for the application to be ready..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Test Serve endpoints
|
||||
run: |
|
||||
curl -s http://localhost:3003
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
|
||||
@@ -140,7 +140,7 @@ const AppPage = ({}) => {
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
No-Code Action
|
||||
</button>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Free");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Plan to 'Free'
|
||||
</button>
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Paid");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Plan to 'Paid'
|
||||
</button>
|
||||
</div>
|
||||
@@ -215,7 +215,7 @@ const AppPage = ({}) => {
|
||||
onClick={() => {
|
||||
formbricks.setEmail("test@web.com");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Email
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SideNavigation = ({ pathname }) => {
|
||||
return (
|
||||
<li
|
||||
key={heading.text}
|
||||
className={`mb-4 ml-4 text-slate-900 dark:text-white ml-${heading.level === 2 ? 0 : heading.level === 3 ? 4 : 6}`}>
|
||||
className={`mb-4 ml-4 text-slate-900 dark:text-white ml-${heading.level === 2 ? 0 : heading.level === 3 ? 4 : 6}`}>
|
||||
<Link
|
||||
href={`#${heading.id}`}
|
||||
onClick={() => setSelectedId(heading.id)}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
value={field.value}
|
||||
onChange={(email) => field.onChange(email)}
|
||||
placeholder="engineering@acme.com"
|
||||
className=" bg-white"
|
||||
className="bg-white"
|
||||
/>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
|
||||
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
|
||||
<div className="inline-flex">
|
||||
@@ -35,7 +35,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col ">
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
|
||||
{/* <hr className="py-1 text-slate-600" /> */}
|
||||
{questionTypes.map((questionType) => (
|
||||
<button
|
||||
|
||||
@@ -41,7 +41,7 @@ export const BackgroundStylingCard = ({
|
||||
}}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
@@ -108,7 +108,7 @@ export const BackgroundStylingCard = ({
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-center ">
|
||||
<div className="flex flex-col justify-center">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="background.brightness"
|
||||
|
||||
@@ -152,6 +152,7 @@ export const CreateNewActionTab = ({
|
||||
|
||||
reset();
|
||||
resetAllStates();
|
||||
toast.success("Action created successfully");
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const EditThankYouCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group z-20 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -61,7 +61,7 @@ export const EditWelcomeCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -49,7 +49,7 @@ export const HiddenFieldsCard = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group z-10 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -55,7 +55,7 @@ export const QuestionsAudienceTabs = ({
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
tab.id === activeId
|
||||
? " border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
? "border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium"
|
||||
)}
|
||||
|
||||
@@ -315,7 +315,7 @@ export const ResponseOptionsCard = ({
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
@@ -415,7 +415,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Change the message visitors see when the survey is closed."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="headline">Heading</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
@@ -447,7 +447,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Allow only 1 response per survey link."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="row mb-2 flex cursor-default items-center space-x-2">
|
||||
<Label htmlFor="howItWorks">How it works</Label>
|
||||
</div>
|
||||
@@ -487,7 +487,7 @@ export const ResponseOptionsCard = ({
|
||||
/>
|
||||
<Label htmlFor="headline">URL Encryption</Label>
|
||||
<div>
|
||||
<div className="mt-2 flex items-center space-x-1 ">
|
||||
<div className="mt-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="encryption-switch"
|
||||
checked={singleUseEncryption}
|
||||
@@ -515,7 +515,7 @@ export const ResponseOptionsCard = ({
|
||||
description="Only let people with a real email respond."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="howItWorks">How it works</Label>
|
||||
<p className="mb-4 mt-2 text-sm text-slate-500">
|
||||
Respondants will receive the survey link via email.
|
||||
|
||||
@@ -231,7 +231,7 @@ export const SurveyMenuBar = ({
|
||||
const updatedSurvey = { ...localSurvey, name: e.target.value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}
|
||||
className="w-72 border-white hover:border-slate-200 "
|
||||
className="w-72 border-white hover:border-slate-200"
|
||||
/>
|
||||
</div>
|
||||
{responseCount > 0 && (
|
||||
@@ -239,16 +239,14 @@ export const SurveyMenuBar = ({
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AlertTriangleIcon className=" h-5 w-5 text-amber-400" />
|
||||
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={"top"} className="lg:hidden">
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400 ">
|
||||
{cautionText}
|
||||
</p>
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className=" hidden pl-1 text-xs md:text-sm lg:block">{cautionText}</p>
|
||||
<p className="hidden pl-1 text-xs md:text-sm lg:block">{cautionText}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex sm:ml-4 sm:mt-0">
|
||||
|
||||
@@ -275,7 +275,7 @@ export const WhenToSendCard = ({
|
||||
childBorder={true}>
|
||||
<label
|
||||
htmlFor="triggerDelay"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Wait
|
||||
|
||||
@@ -44,7 +44,7 @@ export const TemplateContainerWithPreview = ({
|
||||
value={templateSearch ?? ""}
|
||||
onChange={(e) => setTemplateSearch(e.target.value)}
|
||||
placeholder={"Search..."}
|
||||
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto "
|
||||
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto"
|
||||
type="search"
|
||||
name="search"
|
||||
/>
|
||||
|
||||
@@ -71,23 +71,23 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(attributeClass.createdAt.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(attributeClass.updatedAt.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-xs font-normal text-slate-500">Type</Label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<TagIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(attributeClass.type)}</p>
|
||||
<p className="text-sm text-slate-700">{capitalizeFirstLetter(attributeClass.type)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
|
||||
) : (
|
||||
<>
|
||||
{" "}
|
||||
<ArchiveIcon className="mr-2 h-4 text-slate-600" />
|
||||
<ArchiveIcon className="mr-2 h-4 text-slate-600" />
|
||||
<span>Archive</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const AttributeTableHeading = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="col-span-3 pl-6">Name</div>
|
||||
<div className="hidden text-center sm:block">Created</div>
|
||||
<div className="hidden text-center sm:block">Last Updated</div>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,8 @@ const Loading = () => {
|
||||
<PeopleSecondaryNavigation activeId="attributes" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
|
||||
export const ActivityItemIcon = ({ actionItem }: { actionItem: TAction }) => (
|
||||
<div className="h-12 w-12 rounded-full bg-white p-3 text-slate-500 duration-100 ease-in-out group-hover:scale-110 group-hover:text-slate-600">
|
||||
<div className="h-12 w-12 rounded-full bg-white p-3 text-slate-500 duration-100 ease-in-out group-hover:scale-110 group-hover:text-slate-600">
|
||||
<div>
|
||||
{actionItem.actionClass?.type === "code" && <CodeIcon className="h-5 w-5" />}
|
||||
{actionItem.actionClass?.type === "noCode" && <MousePointerClickIcon className="h-5 w-5" />}
|
||||
@@ -47,7 +47,7 @@ export const ActivityItemPopover = ({
|
||||
{actionItem && (
|
||||
<div>
|
||||
<Label className="font-normal text-slate-400">Action Label</Label>
|
||||
<p className=" mb-2 text-sm font-medium text-slate-900">{actionItem.actionClass!.name}</p>
|
||||
<p className="mb-2 text-sm font-medium text-slate-900">{actionItem.actionClass!.name}</p>
|
||||
<Label className="font-normal text-slate-400">Action Description</Label>
|
||||
<p className="text-sm font-medium text-slate-900">{actionItem.actionClass!.description}</p>
|
||||
<Label className="font-normal text-slate-400">Action Type</Label>
|
||||
|
||||
@@ -66,7 +66,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-700">Attributes</h2>
|
||||
<div>
|
||||
@@ -105,14 +105,14 @@ const Loading = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<span className="animate-pulse text-center">Loading user responses</span>
|
||||
</div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
|
||||
|
||||
@@ -63,7 +63,7 @@ const Page = async ({ params }) => {
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={getPersonIdentifier(person, attributes)} cta={getDeletePersonButton()} />
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<AttributesSection personId={params.personId} />
|
||||
<ResponseSection
|
||||
environment={environment}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const PersonCard = async ({ person }: { person: TPerson }) => {
|
||||
href={`/environments/${person.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
|
||||
@@ -64,7 +64,7 @@ const Page = async ({
|
||||
) : (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">User</div>
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">User ID</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">Email</div>
|
||||
</div>
|
||||
|
||||
@@ -35,13 +35,13 @@ export const SegmentActivityTab = ({ currentSegment }: SegmentActivityTabProps)
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(currentSegment.createdAt?.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(currentSegment.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -121,20 +121,20 @@ export const EventActivityTab = ({
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(actionClass.createdAt?.toString())}
|
||||
</p>
|
||||
</div>{" "}
|
||||
<div>
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(actionClass.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="block text-xs font-normal text-slate-500">Type</Label>
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
<div className="mr-1.5 h-4 w-4 text-slate-600">
|
||||
{actionClass.type === "code" ? (
|
||||
<Code2Icon className="h-5 w-5" />
|
||||
) : actionClass.type === "noCode" ? (
|
||||
@@ -143,7 +143,7 @@ export const EventActivityTab = ({
|
||||
<SparklesIcon className="h-5 w-5" />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 ">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ export const ActionTableHeading = () => {
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-4 pl-6">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-4 pl-6">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
{[...Array(3)].map((_, index) => (
|
||||
|
||||
@@ -286,7 +286,7 @@ export const MainNavigation = ({
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700 transition-opacity duration-200 ",
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700 transition-opacity duration-200",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{product.name}
|
||||
|
||||
@@ -3,16 +3,16 @@ export const NavbarLoading = () => {
|
||||
<div>
|
||||
<div className="flex justify-between space-x-4 px-4 py-2">
|
||||
<div className="flex">
|
||||
<div className=" mx-2 h-8 w-8 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-8 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className=" mx-2 h-8 w-8 animate-pulse rounded-full bg-slate-200"></div>
|
||||
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-8 animate-pulse rounded-full bg-slate-200"></div>
|
||||
<div className="mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const UrlShortenerForm = ({ webAppUrl }: { webAppUrl: string }) => {
|
||||
<form onSubmit={handleSubmit(shortenUrl)}>
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Paste Survey Link</Label>
|
||||
<div className="flex gap-3 ">
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={`${webAppUrl}...`}
|
||||
@@ -92,9 +92,9 @@ export const UrlShortenerForm = ({ webAppUrl }: { webAppUrl: string }) => {
|
||||
{shortUrl && (
|
||||
<div className="w-full space-y-2 rounded-lg">
|
||||
<Label>Short Link</Label>
|
||||
<div className="flex gap-3 ">
|
||||
<div className="flex gap-3">
|
||||
<span
|
||||
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
onClick={() => {
|
||||
if (shortUrl) {
|
||||
copyShortUrlToClipboard();
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
return (
|
||||
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
|
||||
<div className="flex w-full justify-end gap-x-6">
|
||||
<div className=" flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
||||
<span
|
||||
className="cursor-pointer text-slate-500"
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ManageIntegration = ({
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
|
||||
@@ -15,9 +15,9 @@ const Loading = () => {
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 text-center ">Survey</div>
|
||||
<div className="col-span-4 text-center">Survey</div>
|
||||
<div className="col-span-4 text-center">Google Sheet Name</div>
|
||||
<div className="col-span-2 text-center ">Questions</div>
|
||||
<div className="col-span-2 text-center">Questions</div>
|
||||
<div className="col-span-2 text-center">Updated At</div>
|
||||
</div>
|
||||
<div className="grid-cols-7">
|
||||
|
||||
@@ -15,7 +15,7 @@ const Loading = () => {
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-2 text-center ">Survey</div>
|
||||
<div className="col-span-2 text-center">Survey</div>
|
||||
<div className="col-span-2 text-center">Database Name</div>
|
||||
<div className="col-span-2 text-center">Updated At</div>
|
||||
</div>
|
||||
|
||||
@@ -75,13 +75,13 @@ export const WebhookOverviewTab = ({ webhook, surveys }: ActivityTabProps) => {
|
||||
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
|
||||
<div>
|
||||
<Label className="text-xs font-normal text-slate-500">Created on</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(webhook.createdAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className=" text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className=" text-xs text-slate-700">
|
||||
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
|
||||
<p className="text-xs text-slate-700">
|
||||
{convertDateTimeStringShort(webhook.updatedAt?.toString())}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@ export const WebhookTableHeading = () => {
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-3 pl-6 ">Webhook</div>
|
||||
<div className="col-span-3 pl-6">Webhook</div>
|
||||
<div className="col-span-1 text-center">Source</div>
|
||||
<div className="col-span-4 text-center">Surveys</div>
|
||||
<div className="col-span-2 text-center ">Triggers</div>
|
||||
<div className="col-span-2 text-center">Triggers</div>
|
||||
<div className="col-span-2 text-center">Updated</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,13 +8,13 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 ">
|
||||
<div className="rounded-lg px-4">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div
|
||||
@@ -107,7 +107,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -127,7 +127,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
|
||||
@@ -8,13 +8,13 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 ">
|
||||
<div className="rounded-lg px-4">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div
|
||||
@@ -107,7 +107,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -127,7 +127,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
|
||||
@@ -90,7 +90,7 @@ export const EditAPIKeys = ({
|
||||
</div>
|
||||
<div className="grid-cols-9">
|
||||
{apiKeysLocal && apiKeysLocal.length === 0 ? (
|
||||
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400 ">
|
||||
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400">
|
||||
You don't have any API keys yet
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -9,12 +9,12 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const LoadingCard = () => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-gray-100 text-lg font-medium leading-6"></h3>
|
||||
<p className="mt-3 h-4 w-full max-w-80 animate-pulse rounded-lg bg-gray-100 text-sm text-slate-500 "></p>
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-gray-100 text-lg font-medium leading-6"></h3>
|
||||
<p className="mt-3 h-4 w-full max-w-80 animate-pulse rounded-lg bg-gray-100 text-sm text-slate-500"></p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 pt-4 ">
|
||||
<div className="rounded-lg px-4 pt-4">
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 sm:col-span-2">Label</div>
|
||||
@@ -26,7 +26,7 @@ const LoadingCard = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-start">
|
||||
<div className="mt-4 flex h-7 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
|
||||
<div className="mt-4 flex h-7 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
|
||||
Loading
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -111,7 +111,7 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
|
||||
|
||||
<LoadingCard />
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -9,12 +9,12 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-4 py-4 pb-0 pt-2 ">
|
||||
<div className="rounded-lg px-4 py-4 pb-0 pt-2">
|
||||
{skeletonLines.map((line, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
|
||||
@@ -100,7 +100,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -75,7 +75,7 @@ const Loading = () => {
|
||||
<div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Configuration">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{navigation.map((navElem) => (
|
||||
<div
|
||||
@@ -178,7 +178,7 @@ const Loading = () => {
|
||||
<div className="flex cursor-not-allowed select-none">
|
||||
<RadioGroup>
|
||||
{placements.map((placement) => (
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap ">
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem
|
||||
className="cursor-not-allowed select-none"
|
||||
id={placement.value}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
@@ -22,8 +22,8 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" my-8 h-64 animate-pulse rounded-xl bg-slate-200 "></div>
|
||||
<div className=" my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
@@ -22,8 +22,8 @@ const Loading = () => {
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" my-8 h-64 animate-pulse rounded-xl bg-slate-200 "></div>
|
||||
<div className=" my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
|
||||
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -166,7 +166,7 @@ const Page = async ({ params }) => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="my-6 text-sm text-slate-700">
|
||||
<p className="my-6 text-sm text-slate-700">
|
||||
No call needed, no strings attached: Request a free 30-day trial license to test all features
|
||||
by filling out this form:
|
||||
</p>
|
||||
@@ -176,7 +176,7 @@ const Page = async ({ params }) => {
|
||||
target="_blank">
|
||||
Request 30-day Trial License
|
||||
</Button>
|
||||
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
||||
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,11 +69,11 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 bg-slate-50 transition-colors hover:bg-slate-100"
|
||||
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 bg-slate-50 transition-colors hover:bg-slate-100"
|
||||
onClick={() => fileInputRef.current?.click()}>
|
||||
{csvFile ? (
|
||||
<XIcon
|
||||
className="absolute right-4 top-4 h-6 w-6 cursor-pointer text-neutral-500"
|
||||
className="absolute right-4 top-4 h-6 w-6 cursor-pointer text-neutral-500"
|
||||
onClick={removeFile}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -41,7 +41,7 @@ export const MembersInfo = async ({
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
<p>{member.name}</p>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
<div className="ph-no-capture col-span-5 flex flex-col justify-center break-all">
|
||||
{member.email}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ export const OrganizationActions = ({
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
}
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,14 @@ export const IndividualInviteTab = ({
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
}: IndividualInviteTabProps) => {
|
||||
const { register, getValues, handleSubmit, reset, control } = useForm<{
|
||||
const {
|
||||
register,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<{
|
||||
name: string;
|
||||
email: string;
|
||||
role: MembershipRole;
|
||||
@@ -31,13 +38,13 @@ export const IndividualInviteTab = ({
|
||||
const submitEventClass = async () => {
|
||||
const data = getValues();
|
||||
data.role = data.role || MembershipRole.Admin;
|
||||
onSubmit([data]);
|
||||
await onSubmit([data]);
|
||||
setOpen(false);
|
||||
reset();
|
||||
};
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg ">
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="w-full space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="memberNameInput">Full Name</Label>
|
||||
@@ -86,7 +93,7 @@ export const IndividualInviteTab = ({
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" size="sm">
|
||||
<Button variant="darkCTA" type="submit" size="sm" loading={isSubmitting}>
|
||||
Send Invitation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,10 @@ import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
const LoadingCard = ({ title, description, skeletonLines }) => {
|
||||
return (
|
||||
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<div
|
||||
data-testid="members-loading-card"
|
||||
className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
|
||||
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
@@ -50,7 +52,7 @@ const Loading = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 border-b border-slate-200">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr] ">
|
||||
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
|
||||
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
|
||||
{pages.map((navElem) => (
|
||||
<div
|
||||
|
||||
@@ -24,16 +24,16 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4 "}>
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{questionSummary.id}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<MessageSquareTextIcon className="mr-2 h-4 w-4" />
|
||||
Hidden Field
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{questionSummary.responseCount} {questionSummary.responseCount === 1 ? "Response" : "Responses"}
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.value}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.person ? (
|
||||
<Link
|
||||
|
||||
@@ -45,7 +45,7 @@ export const MatrixQuestionSummary = ({
|
||||
<tr>
|
||||
<th className="p-4 pb-3 pt-0 font-medium text-slate-400 dark:border-slate-600 dark:text-slate-200"></th>
|
||||
{columns.map((column) => (
|
||||
<th key={column} className="text-center font-medium ">
|
||||
<th key={column} className="text-center font-medium">
|
||||
<TooltipRenderer tooltipContent={getTooltipContent(column)} shouldRender={true}>
|
||||
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{column}</p>
|
||||
</TooltipRenderer>
|
||||
@@ -56,7 +56,7 @@ export const MatrixQuestionSummary = ({
|
||||
<tbody>
|
||||
{questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
|
||||
<tr key={rowLabel}>
|
||||
<td className=" max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
|
||||
<td className="max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
|
||||
<TooltipRenderer tooltipContent={getTooltipContent(rowLabel)} shouldRender={true}>
|
||||
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{rowLabel}</p>
|
||||
</TooltipRenderer>
|
||||
@@ -74,7 +74,7 @@ export const MatrixQuestionSummary = ({
|
||||
)}>
|
||||
<div
|
||||
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
|
||||
className=" hover:outline-brand-dark m-1 flex h-full w-40 cursor-default items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline">
|
||||
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-default items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline">
|
||||
{percentage}
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
@@ -75,8 +75,8 @@ export const MultipleChoiceSummary = ({
|
||||
{result.others && result.others.length > 0 && (
|
||||
<div className="mt-4 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1 pl-6 ">Other values found</div>
|
||||
<div className="col-span-1 pl-6 ">{surveyType === "app" && "User"}</div>
|
||||
<div className="col-span-1 pl-6">Other values found</div>
|
||||
<div className="col-span-1 pl-6">{surveyType === "app" && "User"}</div>
|
||||
</div>
|
||||
{result.others
|
||||
.filter((otherValue) => otherValue.value !== "")
|
||||
|
||||
@@ -29,7 +29,7 @@ export const PictureChoiceSummary = ({
|
||||
{results.map((result) => (
|
||||
<div key={result.id}>
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal ">
|
||||
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
|
||||
<div className="relative h-32 w-[220px]">
|
||||
<Image
|
||||
src={result.imageUrl}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const QuestionSummaryHeader = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4 "}>
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
|
||||
{formatTextWithSlashes(
|
||||
recallToHeadline(questionSummary.question.headline, survey, true, "default", attributeClasses)[
|
||||
@@ -52,19 +52,19 @@ export const QuestionSummaryHeader = ({
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionType && <questionType.icon className="mr-2 h-4 w-4 " />}
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionType && <questionType.icon className="mr-2 h-4 w-4" />}
|
||||
{questionType ? questionType.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
{showResponses && (
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.responseCount} Responses`}
|
||||
</div>
|
||||
)}
|
||||
{insights}
|
||||
{!questionSummary.question.required && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,19 +70,19 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInitialPageButton}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<Code2Icon className="h-6 w-6 text-slate-700" />
|
||||
Embed survey
|
||||
</button>
|
||||
<Link
|
||||
href={`/environments/${environmentId}//settings/notifications`}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<BellRing className="h-6 w-6 text-slate-700" />
|
||||
Configure alerts
|
||||
</Link>
|
||||
<Link
|
||||
href={`/environments/${environmentId}/integrations`}
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
|
||||
<BlocksIcon className="h-6 w-6 text-slate-700" />
|
||||
Setup integrations
|
||||
</Link>
|
||||
|
||||
@@ -60,12 +60,12 @@ export const ShareSurveyResults = ({
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
className=" text-center"
|
||||
className="text-center"
|
||||
onClick={() => handleUnpublish()}>
|
||||
Unpublish
|
||||
</Button>
|
||||
|
||||
<Button variant="darkCTA" className=" text-center" href={surveyUrl} target="_blank">
|
||||
<Button variant="darkCTA" className="text-center" href={surveyUrl} target="_blank">
|
||||
View site
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
|
||||
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.impressions}</div>
|
||||
<div className=" pl-6 text-center md:px-6">
|
||||
<div className="pl-6 text-center md:px-6">
|
||||
<span className="mr-1.5 font-semibold">{quesDropOff.dropOffCount}</span>
|
||||
<span>({Math.round(quesDropOff.dropOffPercentage)}%)</span>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 ">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
{showEmbed ? (
|
||||
<Button
|
||||
|
||||
@@ -116,7 +116,7 @@ export const QuestionFilterComboBox = ({
|
||||
<div
|
||||
onClick={() => !disabled && !isDisabledComboBox && filterValue && setOpen(true)}
|
||||
className={clsx(
|
||||
"group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm",
|
||||
"group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm",
|
||||
disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer"
|
||||
)}>
|
||||
{filterComboBoxValue && filterComboBoxValue?.length > 0 ? (
|
||||
|
||||
@@ -201,10 +201,10 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[300px] border-slate-200 bg-slate-100 p-6 sm:w-[400px] md:w-[750px] lg:w-[1000px] ">
|
||||
className="w-[300px] border-slate-200 bg-slate-100 p-6 sm:w-[400px] md:w-[750px] lg:w-[1000px]">
|
||||
<div className="mb-8 flex flex-wrap items-start justify-between">
|
||||
<p className="hidden text-lg font-bold text-black sm:block">Show all responses that match</p>
|
||||
<p className="block text-base text-slate-500 sm:hidden">Show all responses where...</p>
|
||||
<p className="block text-base text-slate-500 sm:hidden">Show all responses where...</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-normal text-slate-600">Only completed</label>
|
||||
<Checkbox
|
||||
|
||||
@@ -76,15 +76,15 @@ export const SurveyStatusDropdown = ({
|
||||
</SelectTrigger>
|
||||
</TooltipTrigger>
|
||||
<SelectContent className="bg-white">
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="inProgress">
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="inProgress">
|
||||
<PlayCircleIcon className="-mt-1 mr-1 inline h-5 w-5 text-slate-500 group-hover:text-slate-800" />
|
||||
In-progress
|
||||
</SelectItem>
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="paused">
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="paused">
|
||||
<PauseCircleIcon className="-mt-1 mr-1 inline h-5 w-5 text-slate-500 group-hover:text-slate-800" />
|
||||
Paused
|
||||
</SelectItem>
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="completed">
|
||||
<SelectItem className="group font-normal hover:text-slate-900" value="completed">
|
||||
<CheckCircle2Icon className="-mt-1 mr-1 inline h-5 w-5 text-slate-500 group-hover:text-slate-800" />
|
||||
Completed
|
||||
</SelectItem>
|
||||
|
||||
@@ -6,7 +6,7 @@ import Image from "next/image";
|
||||
export const Testimonial = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center bg-gradient-to-tr from-slate-100 to-slate-300">
|
||||
<div className="3xl:w-2/3 mb-10 space-y-8 px-12 xl:px-20 ">
|
||||
<div className="3xl:w-2/3 mb-10 space-y-8 px-12 xl:px-20">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-slate-800">
|
||||
Turn customer insights into irresistible experiences.
|
||||
|
||||
@@ -244,7 +244,7 @@ export const SigninForm = ({
|
||||
</div>
|
||||
|
||||
{publicSignUpEnabled && !totpLogin && isMultiOrgEnabled && (
|
||||
<div className="mt-9 text-center text-xs ">
|
||||
<div className="mt-9 text-center text-xs">
|
||||
<span className="leading-5 text-slate-500">New to Formbricks?</span>
|
||||
<br />
|
||||
<Link
|
||||
|
||||
@@ -99,7 +99,7 @@ export const SignupForm = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-9 text-center text-xs ">
|
||||
<div className="mt-9 text-center text-xs">
|
||||
<span className="leading-5 text-slate-500">Have an account?</span>
|
||||
<br />
|
||||
<Link
|
||||
|
||||
@@ -8,8 +8,8 @@ export const ContentLayout = ({ headline, description, children }: ContentLayout
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="m-auto flex flex-col gap-7 text-center text-slate-700">
|
||||
<h2 className="text-3xl font-bold ">{headline}</h2>
|
||||
<p className="text-2xl ">{description}</p>
|
||||
<h2 className="text-3xl font-bold">{headline}</h2>
|
||||
<p className="text-2xl">{description}</p>
|
||||
<div className="flex justify-center gap-5 text-xs">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ const checkDatabaseConnection = async () => {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e) {
|
||||
console.error("Database connection error:", e);
|
||||
throw new Error("Database could not be reached");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { actions, users } from "@/playwright/utils/mock";
|
||||
import { Page, expect, test } from "@playwright/test";
|
||||
import { finishOnboarding, login, signUpAndLogin } from "./utils/helper";
|
||||
|
||||
const createNoCodeClickAction = async (
|
||||
page: Page,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
actionName: string,
|
||||
description: string,
|
||||
selector: string
|
||||
) => {
|
||||
await signUpAndLogin(page, username, email, password);
|
||||
await finishOnboarding(page);
|
||||
import { actions } from "@/playwright/utils/mock";
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
const createNoCodeClickAction = async ({
|
||||
page,
|
||||
name,
|
||||
description,
|
||||
selector,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
description: string;
|
||||
selector: string;
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
@@ -22,7 +23,7 @@ const createNoCodeClickAction = async (
|
||||
|
||||
// User fills the action name and description
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actionName);
|
||||
await page.getByLabel("What did your user do?").fill(name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(description);
|
||||
@@ -36,27 +37,32 @@ const createNoCodeClickAction = async (
|
||||
await expect(page.locator("[name='noCodeConfig.elementSelector.cssSelector']")).toBeVisible();
|
||||
await page.locator("[name='noCodeConfig.elementSelector.cssSelector']").fill(selector);
|
||||
await page.getByRole("button", { name: "Create action", exact: true }).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const successToast = await page.waitForSelector(".formbricks__toast__success");
|
||||
expect(successToast).toBeTruthy();
|
||||
|
||||
const actionButton = page.getByTitle(name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
};
|
||||
|
||||
const createNoCodePageViewAction = async (
|
||||
page: Page,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
actionName: string,
|
||||
description: string,
|
||||
const createNoCodePageViewAction = async ({
|
||||
page,
|
||||
name,
|
||||
description,
|
||||
matcher,
|
||||
noCodeType,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
description: string;
|
||||
matcher: {
|
||||
label: string;
|
||||
value: string;
|
||||
},
|
||||
testURL: string,
|
||||
noCodeType: string
|
||||
) => {
|
||||
await signUpAndLogin(page, username, email, password);
|
||||
await finishOnboarding(page);
|
||||
|
||||
};
|
||||
noCodeType: string;
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
@@ -65,7 +71,7 @@ const createNoCodePageViewAction = async (
|
||||
|
||||
// User fills the action name and description
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actionName);
|
||||
await page.getByLabel("What did your user do?").fill(name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(description);
|
||||
@@ -83,30 +89,29 @@ const createNoCodePageViewAction = async (
|
||||
// User fills the Page URL to track
|
||||
await page.locator("[name='noCodeConfig.urlFilters.0.value']").fill(matcher.value);
|
||||
|
||||
// User fills the Test URL to track
|
||||
await page.locator("[name='noCodeConfig.urlFilters.testUrl']").fill(testURL);
|
||||
|
||||
// User clicks the Test Match button
|
||||
await page.getByRole("button", { name: "Test Match", exact: true }).click();
|
||||
|
||||
// User clicks the Create Action button
|
||||
await page.getByRole("button", { name: "Create action", exact: true }).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const successToast = await page.waitForSelector(".formbricks__toast__success");
|
||||
expect(successToast).toBeTruthy();
|
||||
|
||||
const actionButton = page.getByTitle(name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
};
|
||||
|
||||
const createNoCodeAction = async (
|
||||
page: Page,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
actionName: string,
|
||||
description: string,
|
||||
noCodeType: string
|
||||
) => {
|
||||
await signUpAndLogin(page, username, email, password);
|
||||
await finishOnboarding(page);
|
||||
|
||||
const createNoCodeAction = async ({
|
||||
name,
|
||||
description,
|
||||
noCodeType,
|
||||
page,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
description: string;
|
||||
noCodeType: string;
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
@@ -115,7 +120,7 @@ const createNoCodeAction = async (
|
||||
|
||||
// User fills the action name and description
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actionName);
|
||||
await page.getByLabel("What did your user do?").fill(name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(description);
|
||||
@@ -125,8 +130,50 @@ const createNoCodeAction = async (
|
||||
|
||||
// User clicks the Create Action button
|
||||
await page.getByRole("button", { name: "Create action", exact: true }).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const successToast = await page.waitForSelector(".formbricks__toast__success");
|
||||
expect(successToast).toBeTruthy();
|
||||
|
||||
const actionButton = page.getByTitle(name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
};
|
||||
|
||||
const createCodeAction = async ({
|
||||
description,
|
||||
key,
|
||||
name,
|
||||
page,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
description: string;
|
||||
key: string;
|
||||
}) => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
// Add Action button
|
||||
await page.getByRole("button", { name: "Add Action" }).click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(description);
|
||||
|
||||
// User selects the Code tab
|
||||
await page.getByText("Code", { exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("Key")).toBeVisible();
|
||||
await page.getByLabel("Key").fill(key);
|
||||
|
||||
await page.getByRole("button", { name: "Create action", exact: true }).click();
|
||||
|
||||
const successToast = await page.waitForSelector(".formbricks__toast__success");
|
||||
expect(successToast).toBeTruthy();
|
||||
|
||||
const actionButton = page.getByTitle(name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
};
|
||||
|
||||
const getActionButtonLocator = (page: Page, actionName: string) => {
|
||||
@@ -134,248 +181,245 @@ const getActionButtonLocator = (page: Page, actionName: string) => {
|
||||
};
|
||||
|
||||
test.describe("Create and Edit No Code Click Action", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name: username } = users.action[0];
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
test("Create No Code Click Action by CSS Selector", async ({ page }) => {
|
||||
await createNoCodeClickAction(
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
actions.create.noCode.click.name,
|
||||
actions.create.noCode.click.description,
|
||||
actions.create.noCode.click.selector
|
||||
);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Edit No Code Click Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
test("Create and Edit No Code Click Action by CSS Selector", async ({ page }) => {
|
||||
await test.step("Create No Code Click Action", async () => {
|
||||
await createNoCodeClickAction({
|
||||
page,
|
||||
name: actions.create.noCode.click.name,
|
||||
description: actions.create.noCode.click.description,
|
||||
selector: actions.create.noCode.click.selector,
|
||||
});
|
||||
});
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.click.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
await test.step("Edit No Code Click Action", async () => {
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.click.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.click.name);
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.click.name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.click.description);
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.click.description);
|
||||
|
||||
await expect(page.locator("[name='noCodeConfig.elementSelector.cssSelector']")).toBeVisible();
|
||||
await page
|
||||
.locator("[name='noCodeConfig.elementSelector.cssSelector']")
|
||||
.fill(actions.edit.noCode.click.selector);
|
||||
await expect(page.locator("[name='noCodeConfig.elementSelector.cssSelector']")).toBeVisible();
|
||||
await page
|
||||
.locator("[name='noCodeConfig.elementSelector.cssSelector']")
|
||||
.fill(actions.edit.noCode.click.selector);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create and Edit No Code Page view Action", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name: username } = users.action[1];
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
test("Create No Code Page view Action", async ({ page }) => {
|
||||
await createNoCodePageViewAction(
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
actions.create.noCode.pageView.name,
|
||||
actions.create.noCode.pageView.description,
|
||||
actions.create.noCode.pageView.matcher,
|
||||
actions.create.noCode.pageView.testURL,
|
||||
"Page View"
|
||||
);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Edit No Code Page view Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
test("Create and Edit No Code Page view Action", async ({ page }) => {
|
||||
await test.step("Create No Code Page view Action", async () => {
|
||||
await createNoCodePageViewAction({
|
||||
page,
|
||||
name: actions.create.noCode.pageView.name,
|
||||
description: actions.create.noCode.pageView.description,
|
||||
matcher: actions.create.noCode.pageView.matcher,
|
||||
noCodeType: "Page View",
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
await test.step("Edit No Code Page view Action", async () => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.pageView.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.pageView.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.pageView.name);
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.pageView.name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.pageView.description);
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.pageView.description);
|
||||
|
||||
await expect(page.locator("[name='noCodeConfig.urlFilters.0.rule']")).toBeVisible();
|
||||
await page
|
||||
.locator("[name='noCodeConfig.urlFilters.0.rule']")
|
||||
.selectOption({ label: actions.edit.noCode.pageView.matcher.label });
|
||||
await expect(page.locator("[name='noCodeConfig.urlFilters.0.rule']")).toBeVisible();
|
||||
await page
|
||||
.locator("[name='noCodeConfig.urlFilters.0.rule']")
|
||||
.selectOption({ label: actions.edit.noCode.pageView.matcher.label });
|
||||
|
||||
await page
|
||||
.locator("[name='noCodeConfig.urlFilters.0.value']")
|
||||
.fill(actions.edit.noCode.pageView.matcher.value);
|
||||
await page
|
||||
.locator("[name='noCodeConfig.urlFilters.0.value']")
|
||||
.fill(actions.edit.noCode.pageView.matcher.value);
|
||||
|
||||
await page.locator("[name='noCodeConfig.urlFilters.testUrl']").fill(actions.edit.noCode.pageView.testURL);
|
||||
await page.getByRole("button", { name: "Test Match", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
await page
|
||||
.locator("[name='noCodeConfig.urlFilters.testUrl']")
|
||||
.fill(actions.edit.noCode.pageView.testURL);
|
||||
await page.getByRole("button", { name: "Test Match", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create and Edit No Code Exit Intent Action", async () => {
|
||||
const { email, password, name: username } = users.action[2];
|
||||
test("Create No Code Exit Intent Action", async ({ page }) => {
|
||||
await createNoCodeAction(
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
actions.create.noCode.exitIntent.name,
|
||||
actions.create.noCode.exitIntent.description,
|
||||
"Exit Intent"
|
||||
);
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Edit No Code Exit Intent Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
test("Create and Edit No Code Exit Intent Action", async ({ page }) => {
|
||||
await test.step("Create No Code Exit Intent Action", async () => {
|
||||
await createNoCodeAction({
|
||||
page,
|
||||
name: actions.create.noCode.exitIntent.name,
|
||||
description: actions.create.noCode.exitIntent.description,
|
||||
noCodeType: "Exit Intent",
|
||||
});
|
||||
});
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.exitIntent.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
await test.step("Edit No Code Exit Intent Action", async () => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.exitIntent.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.exitIntent.name);
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.exitIntent.description);
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.exitIntent.name);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode.exitIntent.description);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create and Edit No Code 50% scroll Action", async () => {
|
||||
const { email, password, name: username } = users.action[3];
|
||||
test("Create No Code 50% scroll Action", async ({ page }) => {
|
||||
await createNoCodeAction(
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
actions.create.noCode["fiftyPercentScroll"].name,
|
||||
actions.create.noCode["fiftyPercentScroll"].description,
|
||||
"50% Scroll"
|
||||
);
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Edit No Code 50% scroll Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
test("Create and Edit No Code 50% scroll Action", async ({ page }) => {
|
||||
await test.step("Create No Code 50% scroll Action", async () => {
|
||||
await createNoCodeAction({
|
||||
page,
|
||||
name: actions.create.noCode["fiftyPercentScroll"].name,
|
||||
description: actions.create.noCode["fiftyPercentScroll"].description,
|
||||
noCodeType: "50% Scroll",
|
||||
});
|
||||
});
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode["fiftyPercentScroll"].name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
await test.step("Edit No Code 50% scroll Action", async () => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
const actionButton = getActionButtonLocator(page, actions.create.noCode["fiftyPercentScroll"].name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode["fiftyPercentScroll"].name);
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode["fiftyPercentScroll"].description);
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode["fiftyPercentScroll"].name);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.noCode["fiftyPercentScroll"].description);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create and Edit Code Action", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name: username } = users.action[4];
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
test("Create Code Action", async ({ page }) => {
|
||||
await signUpAndLogin(page, username, email, password);
|
||||
await finishOnboarding(page);
|
||||
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
// Add Action button
|
||||
await page.getByRole("button", { name: "Add Action" }).click();
|
||||
|
||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||
await page.getByLabel("What did your user do?").fill(actions.create.code.name);
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.create.code.description);
|
||||
|
||||
// User selects the Code tab
|
||||
await page.getByText("Code", { exact: true }).click();
|
||||
|
||||
await expect(page.getByLabel("Key")).toBeVisible();
|
||||
await page.getByLabel("Key").fill(actions.create.code.key);
|
||||
|
||||
await page.getByRole("button", { name: "Create action", exact: true }).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Edit Code Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
test("Create and Edit Code Action", async ({ page }) => {
|
||||
await test.step("Create Code Action", async () => {
|
||||
await createCodeAction({
|
||||
page,
|
||||
name: actions.create.code.name,
|
||||
description: actions.create.code.description,
|
||||
key: actions.create.code.key,
|
||||
});
|
||||
});
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.create.code.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
await test.step("Edit Code Action", async () => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
const actionButton = getActionButtonLocator(page, actions.create.code.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.code.description);
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
await expect(page.getByLabel("Description")).toBeVisible();
|
||||
await page.getByLabel("Description").fill(actions.edit.code.description);
|
||||
|
||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create and Delete Action", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name: username } = users.action[5];
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
test("Create Action", async ({ page }) => {
|
||||
await createNoCodeClickAction(
|
||||
page,
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
actions.delete.noCode.name,
|
||||
actions.delete.noCode.description,
|
||||
actions.delete.noCode.selector
|
||||
);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
|
||||
test("Delete Action", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
test("Create and Delete Action", async ({ page }) => {
|
||||
await test.step("Create Action", async () => {
|
||||
await createNoCodeClickAction({
|
||||
page,
|
||||
name: actions.delete.noCode.name,
|
||||
description: actions.delete.noCode.description,
|
||||
selector: actions.delete.noCode.selector,
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
await test.step("Delete Action", async () => {
|
||||
await page.getByRole("link", { name: "Actions" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||
|
||||
const actionButton = getActionButtonLocator(page, actions.delete.noCode.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
const actionButton = getActionButtonLocator(page, actions.delete.noCode.name);
|
||||
await expect(actionButton).toBeVisible();
|
||||
await actionButton.click();
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await expect(page.locator("#deleteActionModalTrigger")).toBeVisible();
|
||||
await page.locator("#deleteActionModalTrigger").click();
|
||||
await expect(page.locator("#deleteActionModalTrigger")).toBeVisible();
|
||||
await page.locator("#deleteActionModalTrigger").click();
|
||||
|
||||
await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,115 +1,116 @@
|
||||
import { finishOnboarding, signUpAndLogin } from "@/playwright/utils/helper";
|
||||
import { users } from "@/playwright/utils/mock";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const { name, email, password } = users.survey[2];
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "../../lib/fixtures";
|
||||
|
||||
test.describe("API Tests", () => {
|
||||
let surveyId: string;
|
||||
let environmentId: string;
|
||||
let apiKey: string;
|
||||
test("Copy API Key for API Calls", async ({ page }) => {
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await finishOnboarding(page);
|
||||
|
||||
test("API Tests", async ({ page, users, request }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
environmentId =
|
||||
/\/environments\/([^/]+)\/surveys/.exec(page.url())?.[1] ??
|
||||
(() => {
|
||||
throw new Error("Unable to parse environmentId from URL");
|
||||
})();
|
||||
|
||||
await page.goto(`/environments/${environmentId}/product/api-keys`);
|
||||
await test.step("Copy API Key", async () => {
|
||||
environmentId =
|
||||
/\/environments\/([^/]+)\/surveys/.exec(page.url())?.[1] ??
|
||||
(() => {
|
||||
throw new Error("Unable to parse environmentId from URL");
|
||||
})();
|
||||
|
||||
await page.getByRole("button", { name: "Add Production API Key" }).isVisible();
|
||||
await page.getByRole("button", { name: "Add Production API Key" }).click();
|
||||
await page.getByPlaceholder("e.g. GitHub, PostHog, Slack").fill("E2E Test API Key");
|
||||
await page.getByRole("button", { name: "Add API Key" }).click();
|
||||
await page.locator(".copyApiKeyIcon").click();
|
||||
await page.goto(`/environments/${environmentId}/product/api-keys`);
|
||||
|
||||
apiKey = await page.evaluate("navigator.clipboard.readText()");
|
||||
});
|
||||
await page.getByRole("button", { name: "Add Production API Key" }).isVisible();
|
||||
await page.getByRole("button", { name: "Add Production API Key" }).click();
|
||||
await page.getByPlaceholder("e.g. GitHub, PostHog, Slack").fill("E2E Test API Key");
|
||||
await page.getByRole("button", { name: "Add API Key" }).click();
|
||||
await page.locator(".copyApiKeyIcon").click();
|
||||
|
||||
test("Create Survey from API", async ({ request }) => {
|
||||
const response = await request.post(`/api/v1/management/surveys`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
data: {
|
||||
environmentId: environmentId,
|
||||
type: "link",
|
||||
name: "My new Survey from API",
|
||||
},
|
||||
apiKey = await page.evaluate("navigator.clipboard.readText()");
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My new Survey from API");
|
||||
expect(responseBody.data.environmentId).toEqual(environmentId);
|
||||
});
|
||||
await test.step("Create Survey from API", async () => {
|
||||
const response = await request.post(`/api/v1/management/surveys`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
data: {
|
||||
environmentId: environmentId,
|
||||
type: "link",
|
||||
name: "My new Survey from API",
|
||||
},
|
||||
});
|
||||
|
||||
test("List Surveys from API", async ({ request }) => {
|
||||
const response = await request.get(`/api/v1/management/surveys`, {
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
|
||||
const surveyCount = responseBody.data.length;
|
||||
expect(surveyCount).toEqual(1);
|
||||
|
||||
surveyId = responseBody.data[0].id;
|
||||
});
|
||||
|
||||
test("Get Survey by ID from API", async ({ request }) => {
|
||||
const responseSurvey = await request.get(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(responseSurvey.ok()).toBeTruthy();
|
||||
const responseBodySurvey = await responseSurvey.json();
|
||||
|
||||
expect(responseBodySurvey.data.id).toEqual(surveyId);
|
||||
});
|
||||
|
||||
test("Updated Survey by ID from API", async ({ request }) => {
|
||||
const response = await request.put(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
data: {
|
||||
name: "My updated Survey from API",
|
||||
},
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My new Survey from API");
|
||||
expect(responseBody.data.environmentId).toEqual(environmentId);
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My updated Survey from API");
|
||||
});
|
||||
await test.step("List Surveys from API", async () => {
|
||||
const response = await request.get(`/api/v1/management/surveys`, {
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
|
||||
test("Delete Survey by ID from API", async ({ request }) => {
|
||||
const response = await request.delete(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My updated Survey from API");
|
||||
const surveyCount = responseBody.data.length;
|
||||
expect(surveyCount).toEqual(1);
|
||||
|
||||
const responseSurvey = await request.get(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
surveyId = responseBody.data[0].id;
|
||||
});
|
||||
|
||||
await test.step("Get Survey by ID from API", async () => {
|
||||
const responseSurvey = await request.get(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(responseSurvey.ok()).toBeTruthy();
|
||||
const responseBodySurvey = await responseSurvey.json();
|
||||
|
||||
expect(responseBodySurvey.data.id).toEqual(surveyId);
|
||||
});
|
||||
|
||||
await test.step("Updated Survey by ID from API", async () => {
|
||||
const response = await request.put(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
data: {
|
||||
name: "My updated Survey from API",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My updated Survey from API");
|
||||
});
|
||||
|
||||
await test.step("Delete Survey by ID from API", async () => {
|
||||
const response = await request.delete(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.name).toEqual("My updated Survey from API");
|
||||
|
||||
const responseSurvey = await request.get(`/api/v1/management/surveys/${surveyId}`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
});
|
||||
expect(responseSurvey.ok()).toBeFalsy();
|
||||
});
|
||||
expect(responseSurvey.ok()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
128
apps/web/playwright/fixtures/users.ts
Normal file
128
apps/web/playwright/fixtures/users.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { Page } from "playwright";
|
||||
import { TestInfo } from "playwright/test";
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
export const login = async (user: Prisma.UserGetPayload<{ include: { memberships: true } }>, page: Page) => {
|
||||
const csrfToken = await page
|
||||
.context()
|
||||
.request.get("/api/auth/csrf")
|
||||
.then((response) => response.json())
|
||||
.then((json) => json.csrfToken);
|
||||
const data = {
|
||||
email: user.email,
|
||||
password: user.name,
|
||||
callbackURL: "/",
|
||||
redirect: "true",
|
||||
json: "true",
|
||||
csrfToken,
|
||||
};
|
||||
|
||||
await page.context().request.post("/api/auth/callback/credentials", {
|
||||
data,
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
};
|
||||
|
||||
export const createUserFixture = (
|
||||
user: Prisma.UserGetPayload<{ include: { memberships: true } }>,
|
||||
page: Page
|
||||
) => {
|
||||
return {
|
||||
login: async () => {
|
||||
await login(user, page);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type UserFixture = ReturnType<typeof createUserFixture>;
|
||||
|
||||
export const createUsersFixture = (page: Page, workerInfo: TestInfo) => {
|
||||
const store: { users: UserFixture[] } = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
return {
|
||||
create: async (params?: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
organizationName?: string;
|
||||
productName?: string;
|
||||
withoutProduct?: boolean;
|
||||
}) => {
|
||||
const uname = params?.name ?? `user-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
const userEmail = params?.email ?? `${uname}@example.com`;
|
||||
const hashedPassword = await bcrypt.hash(uname, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: uname,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
memberships: {
|
||||
create: {
|
||||
organization: {
|
||||
create: {
|
||||
name: params?.organizationName ?? "My Organization",
|
||||
billing: {
|
||||
plan: "free",
|
||||
limits: { monthly: { responses: 500, miu: 1000 } },
|
||||
stripeCustomerId: null,
|
||||
periodStart: new Date(),
|
||||
period: "monthly",
|
||||
},
|
||||
...(!params?.withoutProduct && {
|
||||
products: {
|
||||
create: {
|
||||
name: params?.productName ?? "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: "development",
|
||||
actionClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "production",
|
||||
actionClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
role: "owner",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { memberships: true },
|
||||
});
|
||||
|
||||
const userFixture = createUserFixture(user, page);
|
||||
|
||||
store.users.push(userFixture);
|
||||
|
||||
return userFixture;
|
||||
},
|
||||
get: () => store.users,
|
||||
};
|
||||
};
|
||||
@@ -1,144 +1,129 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { finishOnboarding, login, replaceEnvironmentIdInHtml, signUpAndLogin } from "./utils/helper";
|
||||
import { users } from "./utils/mock";
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { replaceEnvironmentIdInHtml } from "./utils/helper";
|
||||
|
||||
test.describe("JS Package Test", async () => {
|
||||
const { name, email, password } = users.js[0];
|
||||
let environmentId: string;
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("Admin creates an In-App Survey", async ({ page }) => {
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await finishOnboarding(page, "app");
|
||||
test("Tests", async ({ page, users }) => {
|
||||
await test.step("Admin creates an In-App Survey", async () => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).isVisible();
|
||||
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
await page.getByRole("button", { name: "Use this template" }).isVisible();
|
||||
await page.getByRole("button", { name: "Use this template" }).click();
|
||||
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).isVisible();
|
||||
await page.getByRole("heading", { name: "Product Market Fit (Superhuman)" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Use this template" }).isVisible();
|
||||
await page.getByRole("button", { name: "Use this template" }).click();
|
||||
|
||||
await expect(page.locator("#howToSendCardTrigger")).toBeVisible();
|
||||
await page.locator("#howToSendCardTrigger").click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/edit/);
|
||||
|
||||
await expect(page.locator("#howToSendCardOption-app")).toBeVisible();
|
||||
await page.locator("#howToSendCardOption-app").click();
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Add action" }).click();
|
||||
await page.getByText("New SessionGets fired when a").click();
|
||||
await expect(page.locator("#howToSendCardTrigger")).toBeVisible();
|
||||
await page.locator("#howToSendCardTrigger").click();
|
||||
|
||||
await page.locator("#recontactOptionsCardTrigger").click();
|
||||
await expect(page.locator("#howToSendCardOption-app")).toBeVisible();
|
||||
await page.locator("#howToSendCardOption-app").click();
|
||||
|
||||
await page.locator("label").filter({ hasText: "Keep showing while conditions" }).click();
|
||||
await page.locator("#recontactDays").check();
|
||||
await page.locator("#whenToSendCardTrigger").click();
|
||||
|
||||
await page.getByRole("button", { name: "Publish" }).click();
|
||||
await page.getByRole("button", { name: "Add action" }).click();
|
||||
await page.getByText("New SessionGets fired when a").click();
|
||||
|
||||
environmentId =
|
||||
/\/environments\/([^/]+)\/surveys/.exec(page.url())?.[1] ??
|
||||
(() => {
|
||||
throw new Error("Unable to parse environmentId from URL");
|
||||
})();
|
||||
await page.locator("#recontactOptionsCardTrigger").click();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary/);
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator("label").filter({ hasText: "Keep showing while conditions" }).click();
|
||||
await page.locator("#recontactDays").check();
|
||||
|
||||
await expect(page.getByRole("link", { name: "Surveys" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Surveys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Surveys" })).toBeVisible();
|
||||
});
|
||||
await page.getByRole("button", { name: "Publish" }).click();
|
||||
|
||||
test("JS Display Survey on Page", async ({ page }) => {
|
||||
let currentDir = process.cwd();
|
||||
let htmlFilePath = currentDir + "/packages/js/index.html";
|
||||
environmentId =
|
||||
/\/environments\/([^/]+)\/surveys/.exec(page.url())?.[1] ??
|
||||
(() => {
|
||||
throw new Error("Unable to parse environmentId from URL");
|
||||
})();
|
||||
|
||||
let htmlFile = replaceEnvironmentIdInHtml(htmlFilePath, environmentId);
|
||||
await page.goto(htmlFile);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary/);
|
||||
|
||||
// Formbricks In App Sync has happened
|
||||
const syncApi = await page.waitForResponse((response) => response.url().includes("/app/sync"));
|
||||
expect(syncApi.status()).toBe(200);
|
||||
// await expect(page.getByRole("link", { name: "Surveys" })).toBeVisible();
|
||||
// await page.getByRole("link", { name: "Surveys" }).click();
|
||||
|
||||
// Formbricks Modal exists in the DOM
|
||||
await expect(page.locator("#formbricks-modal-container")).toHaveCount(1);
|
||||
// await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
// const displayApi = await page.waitForResponse((response) => response.url().includes("/display"));
|
||||
// expect(displayApi.status()).toBe(200);
|
||||
// await expect(page.getByRole("heading", { name: "Surveys" })).toBeVisible();
|
||||
});
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(
|
||||
page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
await test.step("JS display survey on page and submit response", async () => {
|
||||
let currentDir = process.cwd();
|
||||
let htmlFilePath = currentDir + "/packages/js/index.html";
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(1500);
|
||||
});
|
||||
let htmlFile = replaceEnvironmentIdInHtml(htmlFilePath, environmentId);
|
||||
await page.goto(htmlFile);
|
||||
|
||||
test("JS submits Response to Survey", async ({ page }) => {
|
||||
let currentDir = process.cwd();
|
||||
let htmlFilePath = currentDir + "/packages/js/index.html";
|
||||
// Formbricks In App Sync has happened
|
||||
const syncApi = await page.waitForResponse((response) => response.url().includes("/app/sync"));
|
||||
expect(syncApi.status()).toBe(200);
|
||||
|
||||
let htmlFile = "file:///" + htmlFilePath;
|
||||
// Formbricks Modal exists in the DOM
|
||||
await expect(page.locator("#formbricks-modal-container")).toHaveCount(1);
|
||||
|
||||
await page.goto(htmlFile);
|
||||
// const displayApi = await page.waitForResponse((response) => response.url().includes("/display"));
|
||||
// expect(displayApi.status()).toBe(200);
|
||||
|
||||
// Formbricks In App Sync has happened
|
||||
const syncApi = await page.waitForResponse((response) => response.url().includes("/app/sync"));
|
||||
expect(syncApi.status()).toBe(200);
|
||||
// Formbricks Modal exists in the DOM
|
||||
// await expect(page.locator("#formbricks-modal-container")).toHaveCount(1);
|
||||
|
||||
// Formbricks Modal exists in the DOM
|
||||
await expect(page.locator("#formbricks-modal-container")).toHaveCount(1);
|
||||
// Formbricks Modal is visible
|
||||
await expect(
|
||||
page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(
|
||||
page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
// Fill the Survey
|
||||
await page.getByRole("button", { name: "Happy to help!" }).click();
|
||||
await page.locator("label").filter({ hasText: "Somewhat disappointed" }).click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("label").filter({ hasText: "Founder" }).click();
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
await page
|
||||
.locator("#questionCard-3")
|
||||
.getByLabel("textarea")
|
||||
.fill("People who believe that PMF is necessary");
|
||||
await page.locator("#questionCard-3").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-4").getByLabel("textarea").fill("Much higher response rates!");
|
||||
await page.locator("#questionCard-4").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-5").getByLabel("textarea").fill("Make this end to end test pass!");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
|
||||
// Fill the Survey
|
||||
await page.getByRole("button", { name: "Happy to help!" }).click();
|
||||
await page.locator("label").filter({ hasText: "Somewhat disappointed" }).click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("label").filter({ hasText: "Founder" }).click();
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
await page
|
||||
.locator("#questionCard-3")
|
||||
.getByLabel("textarea")
|
||||
.fill("People who believe that PMF is necessary");
|
||||
await page.locator("#questionCard-3").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-4").getByLabel("textarea").fill("Much higher response rates!");
|
||||
await page.locator("#questionCard-4").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-5").getByLabel("textarea").fill("Make this end to end test pass!");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
await page.getByText("Thank you!").click();
|
||||
// loading spinner -> wait for it to disappear
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
await page.waitForLoadState("networkidle");
|
||||
});
|
||||
|
||||
// Formbricks Modal is not visible
|
||||
await expect(
|
||||
page.locator("#questionCard-6").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
});
|
||||
await test.step("Admin validates Displays & Response", async () => {
|
||||
await page.goto("/");
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
test("Admin validates Displays & Response", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.getByRole("link", { name: "product Market Fit (Superhuman)" }).click();
|
||||
(await page.waitForSelector("text=Responses")).isVisible();
|
||||
|
||||
await page.getByRole("link", { name: "product Market Fit (Superhuman)" }).click();
|
||||
(await page.waitForSelector("text=Responses")).isVisible();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Survey should have 2 Displays
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByText("Impressions2")).toBeVisible();
|
||||
const impressionsCount = await page.getByRole("button", { name: "Impressions" }).innerText();
|
||||
expect(impressionsCount).toEqual("Impressions\n\n1");
|
||||
|
||||
// Survey should have 1 Response
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByRole("button", { name: "Completed50%" })).toBeVisible();
|
||||
await expect(page.getByText("1 Responses", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("CTR50%")).toBeVisible();
|
||||
await expect(page.getByText("Somewhat disappointed100%")).toBeVisible();
|
||||
await expect(page.getByText("Founder100%")).toBeVisible();
|
||||
await expect(page.getByText("People who believe that PMF").first()).toBeVisible();
|
||||
await expect(page.getByText("Much higher response rates!").first()).toBeVisible();
|
||||
await expect(page.getByText("Make this end to end test").first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Responses (1)" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Completed100%" })).toBeVisible();
|
||||
|
||||
await expect(page.getByText("1 Responses", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("CTR100%")).toBeVisible();
|
||||
await expect(page.getByText("Somewhat disappointed100%")).toBeVisible();
|
||||
await expect(page.getByText("Founder100%")).toBeVisible();
|
||||
await expect(page.getByText("People who believe that PMF").first()).toBeVisible();
|
||||
await expect(page.getByText("Much higher response rates!").first()).toBeVisible();
|
||||
await expect(page.getByText("Make this end to end test").first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
13
apps/web/playwright/lib/fixtures.ts
Normal file
13
apps/web/playwright/lib/fixtures.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { test as base } from "@playwright/test";
|
||||
import { createUsersFixture } from "../fixtures/users";
|
||||
|
||||
export interface Fixtures {
|
||||
users: ReturnType<typeof createUsersFixture>;
|
||||
}
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
users: async ({ page }, use, workerInfo) => {
|
||||
const usersFixture = createUsersFixture(page, workerInfo);
|
||||
await use(usersFixture);
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { signUpAndLogin } from "./utils/helper";
|
||||
import { organizations, users } from "./utils/mock";
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { organizations } from "./utils/mock";
|
||||
|
||||
const { productName } = organizations.onboarding[0];
|
||||
|
||||
test.describe("Onboarding Flow Test", async () => {
|
||||
test("link survey", async ({ page }) => {
|
||||
const { name, email, password } = users.onboarding[0];
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
test("link survey", async ({ page, users }) => {
|
||||
const user = await users.create({ withoutProduct: true });
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/);
|
||||
|
||||
await page.getByRole("button", { name: "100% custom branding Anywhere" }).click();
|
||||
@@ -20,9 +21,10 @@ test.describe("Onboarding Flow Test", async () => {
|
||||
await expect(page.getByText(productName)).toBeVisible();
|
||||
});
|
||||
|
||||
test("website survey", async ({ page }) => {
|
||||
const { name, email, password } = users.onboarding[1];
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
test("website survey", async ({ page, users }) => {
|
||||
const user = await users.create({ withoutProduct: true });
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/organizations\/[^/]+\/products\/new\/channel/);
|
||||
|
||||
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
|
||||
|
||||
@@ -1,118 +1,122 @@
|
||||
import { expect, test } from "playwright/test";
|
||||
import { finishOnboarding, login, signUpAndLogin, signupUsingInviteToken } from "./utils/helper";
|
||||
import { invites, users } from "./utils/mock";
|
||||
import { expect } from "playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { finishOnboarding, signUpAndLogin } from "./utils/helper";
|
||||
import { invites, mockUsers } from "./utils/mock";
|
||||
|
||||
test.describe("Invite, accept and remove organization member", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name } = users.organization[0];
|
||||
let inviteLink: string;
|
||||
|
||||
const { email, name } = mockUsers.organization[0];
|
||||
// let inviteLink: string;
|
||||
|
||||
test("Invite organization member", async ({ page }) => {
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await finishOnboarding(page);
|
||||
await signUpAndLogin(page, name, email, name);
|
||||
await finishOnboarding(page, "link");
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
|
||||
const dropdownInnerContentWrapper = page.locator("#userDropdownInnerContentWrapper");
|
||||
await expect(dropdownInnerContentWrapper).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Organization" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/settings\/members/);
|
||||
|
||||
// Add member button
|
||||
await expect(page.getByRole("button", { name: "Add Member" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Add Member" }).click();
|
||||
|
||||
// Fill the member name and email form
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
await page.getByLabel("Full Name").fill(invites.addMember.name);
|
||||
|
||||
await expect(page.getByLabel("Email Address")).toBeVisible();
|
||||
await page.getByLabel("Email Address").fill(invites.addMember.email);
|
||||
|
||||
await page.getByRole("button", { name: "Send Invitation", exact: true }).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test("Copy invite Link", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page.getByText("My Product")).toBeVisible();
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
await test.step("Invite User", async () => {
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
|
||||
const dropdownInnerContentWrapper = page.locator("#userDropdownInnerContentWrapper");
|
||||
await expect(dropdownInnerContentWrapper).toBeVisible();
|
||||
const dropdownInnerContentWrapper = page.locator("#userDropdownInnerContentWrapper");
|
||||
await expect(dropdownInnerContentWrapper).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Organization" }).click();
|
||||
await page.getByRole("link", { name: "Organization" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/settings\/members/);
|
||||
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
await page.locator('[data-testid="members-loading-card"]:first-child').waitFor({ state: "hidden" });
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
// Add member button
|
||||
await expect(page.getByRole("button", { name: "Add Member" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Add Member" }).click();
|
||||
|
||||
const pendingSpan = lastMemberInfo.locator("span").filter({ hasText: "Pending" });
|
||||
await expect(pendingSpan).toBeVisible();
|
||||
// Fill the member name and email form
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
await page.getByLabel("Full Name").fill(invites.addMember.name);
|
||||
|
||||
const shareInviteButton = page.locator(".shareInviteButton").last();
|
||||
await expect(shareInviteButton).toBeVisible();
|
||||
await expect(page.getByLabel("Email Address")).toBeVisible();
|
||||
await page.getByLabel("Email Address").fill(invites.addMember.email);
|
||||
|
||||
await shareInviteButton.click();
|
||||
await page.getByRole("button", { name: "Send Invitation", exact: true }).click();
|
||||
|
||||
const inviteLinkText = page.locator("#inviteLinkText");
|
||||
await expect(inviteLinkText).toBeVisible();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// invite link text is a paragraph, and we need the text inside it
|
||||
const inviteLinkTextContent = await inviteLinkText.textContent();
|
||||
if (inviteLinkTextContent) {
|
||||
inviteLink = inviteLinkTextContent;
|
||||
}
|
||||
// const successToast = await page.waitForSelector(".formbricks__toast__success");
|
||||
// expect(successToast).toBeTruthy();
|
||||
});
|
||||
|
||||
await test.step("Copy invite Link", async () => {
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
const pendingSpan = lastMemberInfo.locator("span").filter({ hasText: "Pending" });
|
||||
await expect(pendingSpan).toBeVisible();
|
||||
|
||||
const shareInviteButton = page.locator(".shareInviteButton").last();
|
||||
await expect(shareInviteButton).toBeVisible();
|
||||
|
||||
await shareInviteButton.click();
|
||||
|
||||
const inviteLinkText = await page.waitForSelector("#inviteLinkText");
|
||||
expect(inviteLinkText).toBeTruthy();
|
||||
|
||||
// invite link text is a paragraph, and we need the text inside it
|
||||
const inviteLinkTextContent = await inviteLinkText.textContent();
|
||||
expect(inviteLinkTextContent).toBeTruthy();
|
||||
// if (inviteLinkTextContent) {
|
||||
// inviteLink = inviteLinkTextContent;
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
test("Accept invite", async ({ page }) => {
|
||||
const { email, name, password } = users.organization[1];
|
||||
page.goto(inviteLink);
|
||||
// test("Accept invite", async ({ page }) => {
|
||||
// const { email, name } = mockUsers.organization[1];
|
||||
// page.goto(inviteLink);
|
||||
|
||||
await page.waitForURL(/\/invite\?token=[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/);
|
||||
// await page.waitForURL(/\/invite\?token=[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/);
|
||||
|
||||
// Create account button
|
||||
await expect(page.getByRole("link", { name: "Create account" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Create account" }).click();
|
||||
// // Create account button
|
||||
// await expect(page.getByRole("link", { name: "Create account" })).toBeVisible();
|
||||
// await page.getByRole("link", { name: "Create account" }).click();
|
||||
|
||||
await signupUsingInviteToken(page, name, email, password);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
});
|
||||
// await signupUsingInviteToken(page, name, email, name);
|
||||
// await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
// });
|
||||
|
||||
test("Remove member", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
// test("Remove member", async ({ page }) => {
|
||||
// await apiLogin(page, email, name);
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
// await page.goto("/");
|
||||
// await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
const dropdownInnerContentWrapper = page.locator("#userDropdownInnerContentWrapper");
|
||||
await expect(dropdownInnerContentWrapper).toBeVisible();
|
||||
// const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
// await expect(dropdownTrigger).toBeVisible();
|
||||
// await dropdownTrigger.click();
|
||||
|
||||
await page.getByRole("link", { name: "Organization" }).click();
|
||||
// const dropdownInnerContentWrapper = page.locator("#userDropdownInnerContentWrapper");
|
||||
// await expect(dropdownInnerContentWrapper).toBeVisible();
|
||||
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
// await page.getByRole("link", { name: "Organization" }).click();
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
// await page.waitForURL(/\/environments\/[^/]+\/settings\/members/);
|
||||
|
||||
const deleteMemberButton = lastMemberInfo.locator("#deleteMemberButton");
|
||||
await expect(deleteMemberButton).toBeVisible();
|
||||
// await page.locator('[data-testid="members-loading-card"]:first-child').waitFor({ state: "hidden" });
|
||||
|
||||
await deleteMemberButton.click();
|
||||
// await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Delete", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
// const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
// await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
await expect(page.getByText("organization2@formbricks.com")).not.toBeVisible();
|
||||
});
|
||||
// const deleteMemberButton = lastMemberInfo.locator("#deleteMemberButton");
|
||||
// await expect(deleteMemberButton).toBeVisible();
|
||||
|
||||
// await deleteMemberButton.click();
|
||||
|
||||
// await expect(page.getByRole("button", { name: "Delete", exact: true })).toBeVisible();
|
||||
// await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { users } from "./utils/mock";
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { mockUsers } from "./utils/mock";
|
||||
|
||||
const { name, email, password } = users.signup[0];
|
||||
const { name, email, password } = mockUsers.signup[0];
|
||||
|
||||
test.describe("Email Signup Flow Test", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
@@ -1,192 +1,199 @@
|
||||
import { surveys, users } from "@/playwright/utils/mock";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createSurvey, finishOnboarding, signUpAndLogin } from "./utils/helper";
|
||||
import { surveys } from "@/playwright/utils/mock";
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { createSurvey } from "./utils/helper";
|
||||
|
||||
test.describe("Survey Create & Submit Response", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
let url: string | null;
|
||||
const { name, email, password } = users.survey[0];
|
||||
|
||||
test("Create Survey", async ({ page }) => {
|
||||
await createSurvey(page, name, email, password, surveys.createAndSubmit);
|
||||
test("Create survey and submit response", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
// Save & Publish Survey
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
await page.locator("#howToSendCardTrigger").click();
|
||||
await expect(page.locator("#howToSendCardOption-link")).toBeVisible();
|
||||
await page.locator("#howToSendCardOption-link").click();
|
||||
await test.step("Create Survey", async () => {
|
||||
await createSurvey(page, surveys.createAndSubmit);
|
||||
|
||||
await page.getByRole("button", { name: "Publish" }).click();
|
||||
// Save & Publish Survey
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
// Get URL
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary$/);
|
||||
await page.getByLabel("Copy survey link to clipboard").click();
|
||||
url = await page.evaluate("navigator.clipboard.readText()");
|
||||
});
|
||||
await page.locator("#howToSendCardTrigger").click();
|
||||
await expect(page.locator("#howToSendCardOption-link")).toBeVisible();
|
||||
await page.locator("#howToSendCardOption-link").click();
|
||||
|
||||
test("Submit Survey Response", async ({ page }) => {
|
||||
await page.goto(url!);
|
||||
await page.waitForURL(/\/s\/[A-Za-z0-9]+$/);
|
||||
await page.getByRole("button", { name: "Publish" }).click();
|
||||
|
||||
// Welcome Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.description)).toBeVisible();
|
||||
await page.locator("#questionCard--1").getByRole("button", { name: "Next" }).click();
|
||||
// Get URL
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary$/);
|
||||
await page.getByLabel("Copy survey link to clipboard").click();
|
||||
url = await page.evaluate("navigator.clipboard.readText()");
|
||||
});
|
||||
|
||||
// Open Text Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.openTextQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.openTextQuestion.description)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)
|
||||
.fill("This is my Open Text answer");
|
||||
await page.locator("#questionCard-0").getByRole("button", { name: "Next" }).click();
|
||||
await test.step("Submit Survey Response", async () => {
|
||||
await page.goto(url!);
|
||||
await page.waitForURL(/\/s\/[A-Za-z0-9]+$/);
|
||||
|
||||
// Single Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[i] })
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.getByText("Other")).toBeVisible();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[0] })
|
||||
.click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
// Welcome Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.description)).toBeVisible();
|
||||
await page.locator("#questionCard--1").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Multi Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
// Open Text Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.openTextQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.openTextQuestion.description)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)
|
||||
.fill("This is my Open Text answer");
|
||||
await page.locator("#questionCard-0").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Single Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[i] })
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.getByText("Other")).toBeVisible();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[0] })
|
||||
.click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Multi Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
.locator("#questionCard-2 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.multiSelectQuestion.options[i] })
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.multiSelectQuestion.options.length; i++) {
|
||||
await page
|
||||
.locator("#questionCard-2 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.multiSelectQuestion.options[i] })
|
||||
.click();
|
||||
}
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Rating Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.description)).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.multiSelectQuestion.options.length; i++) {
|
||||
await page
|
||||
.locator("#questionCard-2 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.multiSelectQuestion.options[i] })
|
||||
.click();
|
||||
}
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.locator("path").nth(3).click();
|
||||
|
||||
// Rating Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.description)).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.locator("path").nth(3).click();
|
||||
// NPS Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.npsQuestion.question)).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
|
||||
// NPS Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.npsQuestion.question)).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await expect(page.locator("#questionCard-4").getByText(`${i}`, { exact: true })).toBeVisible();
|
||||
}
|
||||
await page.locator("#questionCard-4").getByText("8", { exact: true }).click();
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await expect(page.locator("#questionCard-4").getByText(`${i}`, { exact: true })).toBeVisible();
|
||||
}
|
||||
await page.locator("#questionCard-4").getByText("8", { exact: true }).click();
|
||||
// CTA Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ctaQuestion.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: surveys.createAndSubmit.ctaQuestion.buttonLabel })
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: surveys.createAndSubmit.ctaQuestion.buttonLabel }).click();
|
||||
|
||||
// CTA Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ctaQuestion.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: surveys.createAndSubmit.ctaQuestion.buttonLabel })
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: surveys.createAndSubmit.ctaQuestion.buttonLabel }).click();
|
||||
// Consent Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Consent Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
||||
// Picture Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.description)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-1-small.jpg" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-2-small.jpg" })).toBeVisible();
|
||||
await page.getByRole("img", { name: "puppy-1-small.jpg" }).click();
|
||||
await page.locator("#questionCard-7").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Picture Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.description)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-1-small.jpg" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-2-small.jpg" })).toBeVisible();
|
||||
await page.getByRole("img", { name: "puppy-1-small.jpg" }).click();
|
||||
await page.locator("#questionCard-7").getByRole("button", { name: "Next" }).click();
|
||||
// File Upload Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.fileUploadQuestion.question)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("label").filter({ hasText: "Click or drag to upload files." }).locator("div").nth(0)
|
||||
).toBeVisible();
|
||||
await page.locator("input[type=file]").setInputFiles({
|
||||
name: "file.txt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("this is test"),
|
||||
});
|
||||
await page.getByText("Uploading...").waitFor({ state: "hidden" });
|
||||
await page.locator("#questionCard-8").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// File Upload Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.fileUploadQuestion.question)).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("label").filter({ hasText: "Click or drag to upload files." }).locator("div").nth(0)
|
||||
).toBeVisible();
|
||||
await page.locator("input[type=file]").setInputFiles({
|
||||
name: "file.txt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("this is test"),
|
||||
// Matrix Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.matrix.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.matrix.description)).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[0] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[1] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[2] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[0] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[1] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[2] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[3] })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByRole("row", { name: "Rose 🌹" }).getByRole("cell").nth(1).click();
|
||||
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder).fill("This is my Address");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
|
||||
// loading spinner -> wait for it to disappear
|
||||
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
|
||||
|
||||
// Thank You Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.description)).toBeVisible();
|
||||
});
|
||||
await page.getByText("Uploading...").waitFor({ state: "hidden" });
|
||||
await page.locator("#questionCard-8").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Matrix Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.matrix.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.matrix.description)).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[0] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[1] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.rows[2] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[0] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[1] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[2] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[3] })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByRole("row", { name: "Rose 🌹" }).getByRole("cell").nth(1).click();
|
||||
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder).fill("This is my Address");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Thank You Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.description)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Multi Language Survey Create", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { name, email, password } = users.survey[3];
|
||||
test("Create Survey", async ({ page }) => {
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await finishOnboarding(page, "app");
|
||||
test("Create Survey", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
//add a new language
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
|
||||
@@ -55,6 +55,26 @@ export const login = async (page: Page, email: string, password: string): Promis
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
};
|
||||
|
||||
export const apiLogin = async (page: Page, email: string, password: string) => {
|
||||
const csrfToken = await page
|
||||
.context()
|
||||
.request.get("/api/auth/csrf")
|
||||
.then((response) => response.json())
|
||||
.then((json) => json.csrfToken);
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/",
|
||||
redirect: "true",
|
||||
json: "true",
|
||||
csrfToken,
|
||||
};
|
||||
|
||||
return page.context().request.post("/api/auth/callback/credentials", {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const finishOnboarding = async (
|
||||
page: Page,
|
||||
ProductChannel: TProductConfigChannel = "website"
|
||||
@@ -113,21 +133,14 @@ export const signupUsingInviteToken = async (page: Page, name: string, email: st
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
};
|
||||
|
||||
export const createSurvey = async (
|
||||
page: Page,
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
params: CreateSurveyParams
|
||||
) => {
|
||||
export const createSurvey = async (page: Page, params: CreateSurveyParams) => {
|
||||
const addQuestion = "Add QuestionAdd a new question to your survey";
|
||||
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await finishOnboarding(page);
|
||||
|
||||
await page.getByRole("button", { name: "Start from scratch Create a" }).click();
|
||||
await page.getByRole("button", { name: "Create survey", exact: true }).click();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/edit$/);
|
||||
|
||||
// Welcome Card
|
||||
await expect(page.locator("#welcome-toggle")).toBeVisible();
|
||||
await page.getByText("Welcome Card").click();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const users = {
|
||||
export const mockUsers = {
|
||||
signup: [
|
||||
{
|
||||
name: "SignUp Flow User 1",
|
||||
@@ -56,44 +56,36 @@ export const users = {
|
||||
{
|
||||
name: "Action User 1",
|
||||
email: "action1@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
{
|
||||
name: "Action User 2",
|
||||
email: "action2@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
{
|
||||
name: "Action User 3",
|
||||
email: "action3@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
{
|
||||
name: "Action User 4",
|
||||
email: "action4@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
{
|
||||
name: "Action User 5",
|
||||
email: "action5@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
{
|
||||
name: "Action User 6",
|
||||
email: "action6@formbricks.com",
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
],
|
||||
organization: [
|
||||
{
|
||||
name: "Organization User 1",
|
||||
email: "organization1@formbricks.com",
|
||||
password: "Test#1234",
|
||||
},
|
||||
{
|
||||
name: "Organization User 2",
|
||||
email: "organization2@formbricks.com",
|
||||
password: "Test#1234",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -256,7 +248,6 @@ export const actions = {
|
||||
label: "Contains",
|
||||
value: "custom-url",
|
||||
},
|
||||
testURL: "http://localhost:3000/custom-url",
|
||||
},
|
||||
exitIntent: {
|
||||
name: "Create Exit Intent Action",
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DefaultLanguageSelect({
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm">1. Choose the default language for this survey:</p>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-48 ">
|
||||
<div className="w-48">
|
||||
<Select
|
||||
defaultValue={`${defaultLanguage?.code}`}
|
||||
disabled={Boolean(defaultLanguage)}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function LanguageIndicator({
|
||||
</button>
|
||||
{showLanguageDropdown ? (
|
||||
<div
|
||||
className="absolute right-0 z-30 mt-1 space-y-2 rounded-md bg-slate-900 p-1 text-xs text-white "
|
||||
className="absolute right-0 z-30 mt-1 space-y-2 rounded-md bg-slate-900 p-1 text-xs text-white"
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map(
|
||||
(language) =>
|
||||
|
||||
@@ -176,7 +176,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
|
||||
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
|
||||
"group z-10 flex flex-row rounded-lg bg-white text-slate-900 transition-transform duration-300 ease-in-out"
|
||||
)}>
|
||||
<div
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
e.parentNode.insertBefore(t, e),
|
||||
setTimeout(function () {
|
||||
formbricks.init({
|
||||
environmentId: "clxmx0pbw00hj9ed7xxpxlos1",
|
||||
environmentId: "clxvg2bf40005m66n1mrqsi1d",
|
||||
userId: "RANDOM_USER_ID",
|
||||
apiHost: "http://localhost:3000",
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Headline = ({
|
||||
return (
|
||||
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
||||
<div
|
||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}
|
||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}
|
||||
dir="auto">
|
||||
{headline}
|
||||
{!required && (
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export const LoadingSpinner = () => {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const LoadingSpinner = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div
|
||||
data-testid="loading-spinner"
|
||||
className={cn("flex h-full w-full items-center justify-center", className ?? "")}>
|
||||
<svg
|
||||
className="m-2 h-6 w-6 animate-spin text-slate-700"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -40,7 +40,7 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
key={imgUrl}
|
||||
src={imgUrl}
|
||||
alt={altText}
|
||||
className="rounded-custom "
|
||||
className="rounded-custom"
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
class="text-heading relative h-5 w-5 rounded-md hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
class="text-heading relative h-5 w-5 rounded-md hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
<svg
|
||||
class="h-5 w-5 p-0.5"
|
||||
fill="none"
|
||||
|
||||
@@ -208,9 +208,7 @@ export const DateQuestion = ({
|
||||
monthPlaceholder="MM"
|
||||
yearPlaceholder="YYYY"
|
||||
format={question.format ?? "M-d-y"}
|
||||
className={`dp-input-root rounded-custom wrapper-hide ${!datePickerOpen ? "" : "h-[46dvh] sm:h-[34dvh]"}
|
||||
${hideInvalid ? "hide-invalid" : ""}
|
||||
`}
|
||||
className={`dp-input-root rounded-custom wrapper-hide ${!datePickerOpen ? "" : "h-[46dvh] sm:h-[34dvh]"} ${hideInvalid ? "hide-invalid" : ""} `}
|
||||
calendarClassName="calendar-root !bg-input-bg border border-border rounded-custom p-3 h-[46dvh] sm:h-[33dvh] overflow-auto"
|
||||
clearIcon={null}
|
||||
onCalendarOpen={() => {
|
||||
|
||||
@@ -69,7 +69,7 @@ export const FileUploadQuestion = ({
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="w-full ">
|
||||
className="w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
|
||||
@@ -128,7 +128,7 @@ export const OpenTextQuestion = ({
|
||||
handleInputResize(e);
|
||||
}}
|
||||
autoFocus={autoFocusEnabled}
|
||||
className="border-border placeholder:text-placeholder bg-input-bg text-subheading focus:border-brand rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
className="border-border placeholder:text-placeholder bg-input-bg text-subheading focus:border-brand rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
|
||||
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
|
||||
/>
|
||||
|
||||
@@ -117,11 +117,7 @@ export const Button: React.ForwardRefExoticComponent<
|
||||
<>
|
||||
{StartIcon && (
|
||||
<StartIcon
|
||||
className={cn(
|
||||
"flex",
|
||||
size === "icon" ? "h-4 w-4 " : "-ml-1 mr-1 h-3 w-3",
|
||||
startIconClassName || ""
|
||||
)}
|
||||
className={cn("flex", size === "icon" ? "h-4 w-4" : "-ml-1 mr-1 h-3 w-3", startIconClassName || "")}
|
||||
/>
|
||||
)}
|
||||
{props.children}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ColorPicker = ({ color, onChange, containerClass, disabled = false
|
||||
<div className="flex w-full items-center">
|
||||
#
|
||||
<HexColorInput
|
||||
className="ml-2 mr-2 h-10 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
|
||||
className="ml-2 mr-2 h-10 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
|
||||
color={color}
|
||||
onChange={onChange}
|
||||
id="color"
|
||||
|
||||
@@ -90,7 +90,7 @@ const DropdownMenuItem: React.ForwardRefExoticComponent<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
@@ -158,7 +158,7 @@ const DropdownMenuLabel: React.ComponentType<
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold text-slate-900 ", inset && "pl-8", className)}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold text-slate-900", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -172,7 +172,7 @@ const DropdownMenuSeparator: React.ComponentType<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100 ", className)}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -11,7 +11,7 @@ export const AddVariablesDropdown: React.FC<IAddVariablesDropdown> = (props) =>
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="focus:bg-muted pt-[6px]">
|
||||
<div className="items-center ">
|
||||
<div className="items-center">
|
||||
{props.isTextEditor ? (
|
||||
<>
|
||||
<div className="hidden sm:flex">
|
||||
|
||||
@@ -21,15 +21,15 @@ export const EmptySpaceFiller = ({
|
||||
}: EmptySpaceFillerProps) => {
|
||||
if (type === "table") {
|
||||
return (
|
||||
<div className="shadow-xs group rounded-xl border border-slate-100 bg-white p-4 ">
|
||||
<div className="shadow-xs group rounded-xl border border-slate-100 bg-white p-4">
|
||||
<div className="w-full space-y-3">
|
||||
<div className="h-16 w-full rounded-lg bg-slate-50"></div>
|
||||
<div className="flex h-16 w-full flex-col items-center justify-center rounded-lg bg-slate-50 text-slate-700 transition-all duration-300 ease-in-out hover:bg-slate-100 ">
|
||||
<div className="flex h-16 w-full flex-col items-center justify-center rounded-lg bg-slate-50 text-slate-700 transition-all duration-300 ease-in-out hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
</Link>
|
||||
@@ -45,19 +45,19 @@ export const EmptySpaceFiller = ({
|
||||
|
||||
if (type === "response") {
|
||||
return (
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
</Link>
|
||||
@@ -76,19 +76,19 @@ export const EmptySpaceFiller = ({
|
||||
|
||||
if (type === "tag") {
|
||||
return (
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
|
||||
{!widgetSetupCompleted && !noWidgetRequired && (
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
</Link>
|
||||
@@ -106,16 +106,16 @@ export const EmptySpaceFiller = ({
|
||||
if (type === "summary") {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<Skeleton className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<Skeleton className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className=" flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
@@ -124,10 +124,10 @@ export const EmptySpaceFiller = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6 ">
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
|
||||
<div className=" h-6 w-full rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 w-full rounded-full bg-slate-100"></div>
|
||||
@@ -136,7 +136,7 @@ export const EmptySpaceFiller = ({
|
||||
<Link
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
href={`/environments/${environment.id}/product/app-connection`}>
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
<span className="decoration-brand-dark underline transition-all duration-300 ease-in-out">
|
||||
Install Formbricks Widget. <strong>Go to Setup Checklist 👉</strong>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, isInv
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
||||
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
|
||||
className,
|
||||
isInvalid && "border-error focus:border-error border"
|
||||
)}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ModalWithTabs = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center space-x-2 border-b border-slate-200 px-6 ">
|
||||
<div className="flex h-full w-full items-center justify-center space-x-2 border-b border-slate-200 px-6">
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
key={index}
|
||||
|
||||
@@ -31,9 +31,9 @@ const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
className={cn("absolute right-3 top-1/2 -translate-y-1/2 transform")}
|
||||
onClick={togglePasswordVisibility}>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5 text-slate-400 " />
|
||||
<EyeOff className="h-5 w-5 text-slate-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-slate-400 " />
|
||||
<EyeIcon className="h-5 w-5 text-slate-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user