Compare commits

...

12 Commits

Author SHA1 Message Date
review-agent-prime[bot]
096947a93a Edit tests/initial.spec.ts 2023-12-05 09:55:31 +00:00
Dhruwang
e563b3330a added playwright test for signup,login,onboarding 2023-12-05 15:14:50 +05:30
Anjy Gupta
9271e375af feat: add image/color/animation as survey background (#1515)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Neil Chauhan <neilchauhan2@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-12-04 18:22:28 +00:00
Matti Nannt
35a9685b71 fix: let report usage cron run with no-cache headers (#1739) 2023-12-04 17:37:24 +00:00
Matti Nannt
723ea558fa fix: remove person validation in syncSurveys for backwards compatibility (#1737) 2023-12-04 15:24:51 +00:00
Matti Nannt
8a4a635ee3 fix: in-app surveys not pulled correctly when user attributes get set (#1735) 2023-12-04 12:58:36 +00:00
Shubham Palriwala
1a30e9fd11 fix: error handling in next-auth (#1734) 2023-12-04 12:52:08 +00:00
Dhruwang Jariwala
dc8e1c764b fix: empty trigger save issue (#1733) 2023-12-04 11:38:08 +00:00
Dhruwang Jariwala
48e9148728 fix: made ttc and userAgent optional (#1727) 2023-12-04 11:36:31 +00:00
Shubham Palriwala
25525e0b03 fix: docker builds to work with node 20 (#1728) 2023-12-04 11:13:56 +00:00
Shubham Palriwala
9720c0ecba fix: remove configuration option for asset prefix URL (#1729) 2023-12-04 11:13:22 +00:00
Dhruwang Jariwala
33cbe7cf22 fix: eliminate empty attribute filter (#1730) 2023-12-04 11:12:41 +00:00
42 changed files with 925 additions and 188 deletions

View File

@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# This will run the job at 23:00 UTC every day of every month.
- cron: "0 21 * * *"
# This will run the job at 22:00 UTC every day of every month.
- cron: "0 22 * * *"
jobs:
cron-reportUsageToStripe:
env:
@@ -19,4 +19,5 @@ jobs:
curl ${{ env.APP_URL }}/api/cron/report-usage \
-X POST \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
-H 'Cache-Control: no-cache' \
--fail

27
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

6
.gitignore vendored
View File

@@ -44,4 +44,8 @@ packages/database/zod
# nixos stuff
.direnv
Zone.Identifier
Zone.Identifier
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -18,7 +18,6 @@ Formbricks v1.2 ships a lot of features targeting our Link Surveys. We have also
| -------------------- | -------- | ------------------------------ | ----------------------------------------------------------- |
| ENCRYPTION_KEY | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
| SHORT_URL_BASE | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
| ASSET_PREFIX_URL | false | `<your-asset-hosted-base-url>` | Needed if you have a separate URL for hosted assets |
### Deprecated / Removed Environment Variables

View File

@@ -204,6 +204,7 @@ export async function copyToOtherEnvironmentAction(
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
styling: existingSurvey.styling ?? prismaClient.JsonNull,
},
});

View File

@@ -63,7 +63,7 @@ export default function SummaryMetadata({
const ttc = useMemo(() => {
let validTtcResponsesCountAcc = 0; //stores the count of responses that contains a _total value
const ttc = responses.reduce((acc, response) => {
if (response.ttc._total) {
if (response.ttc?._total) {
validTtcResponsesCountAcc++;
return acc + response.ttc._total;
}

View File

@@ -0,0 +1,107 @@
import { TSurvey } from "@formbricks/types/surveys";
import { useState } from "react";
interface AnimatedSurveyBgProps {
localSurvey?: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
}
export default function AnimatedSurveyBg({ localSurvey, handleBgChange }: AnimatedSurveyBgProps) {
const [color, setColor] = useState(localSurvey?.styling?.background?.bg || "#ffff");
const [hoveredVideo, setHoveredVideo] = useState<number | null>(null);
const animationFiles = {
"/animated-bgs/Thumbnails/1_Thumb.mp4": "/animated-bgs/4K/1_4k.mp4",
"/animated-bgs/Thumbnails/2_Thumb.mp4": "/animated-bgs/4K/2_4k.mp4",
"/animated-bgs/Thumbnails/3_Thumb.mp4": "/animated-bgs/4K/3_4k.mp4",
"/animated-bgs/Thumbnails/4_Thumb.mp4": "/animated-bgs/4K/4_4k.mp4",
"/animated-bgs/Thumbnails/5_Thumb.mp4": "/animated-bgs/4K/5_4k.mp4",
"/animated-bgs/Thumbnails/6_Thumb.mp4": "/animated-bgs/4K/6_4k.mp4",
"/animated-bgs/Thumbnails/7_Thumb.mp4": "/animated-bgs/4K/7_4k.mp4",
"/animated-bgs/Thumbnails/8_Thumb.mp4": "/animated-bgs/4K/8_4k.mp4",
"/animated-bgs/Thumbnails/9_Thumb.mp4": "/animated-bgs/4K/9_4k.mp4",
"/animated-bgs/Thumbnails/10_Thumb.mp4": "/animated-bgs/4K/10_4k.mp4",
"/animated-bgs/Thumbnails/11_Thumb.mp4": "/animated-bgs/4K/11_4k.mp4",
"/animated-bgs/Thumbnails/12_Thumb.mp4": "/animated-bgs/4K/12_4k.mp4",
"/animated-bgs/Thumbnails/13_Thumb.mp4": "/animated-bgs/4K/13_4k.mp4",
"/animated-bgs/Thumbnails/14_Thumb.mp4": "/animated-bgs/4K/14_4k.mp4",
"/animated-bgs/Thumbnails/15_Thumb.mp4": "/animated-bgs/4K/15_4k.mp4",
"/animated-bgs/Thumbnails/16_Thumb.mp4": "/animated-bgs/4K/16_4k.mp4",
"/animated-bgs/Thumbnails/17_Thumb.mp4": "/animated-bgs/4K/17_4k.mp4",
"/animated-bgs/Thumbnails/18_Thumb.mp4": "/animated-bgs/4K/18_4k.mp4",
"/animated-bgs/Thumbnails/19_Thumb.mp4": "/animated-bgs/4K/19_4k.mp4",
"/animated-bgs/Thumbnails/20_Thumb.mp4": "/animated-bgs/4K/20_4k.mp4",
"/animated-bgs/Thumbnails/21_Thumb.mp4": "/animated-bgs/4K/21_4k.mp4",
"/animated-bgs/Thumbnails/22_Thumb.mp4": "/animated-bgs/4K/22_4k.mp4",
"/animated-bgs/Thumbnails/23_Thumb.mp4": "/animated-bgs/4K/23_4k.mp4",
"/animated-bgs/Thumbnails/24_Thumb.mp4": "/animated-bgs/4K/24_4k.mp4",
"/animated-bgs/Thumbnails/25_Thumb.mp4": "/animated-bgs/4K/25_4k.mp4",
"/animated-bgs/Thumbnails/26_Thumb.mp4": "/animated-bgs/4K/26_4k.mp4",
"/animated-bgs/Thumbnails/27_Thumb.mp4": "/animated-bgs/4K/27_4k.mp4",
"/animated-bgs/Thumbnails/28_Thumb.mp4": "/animated-bgs/4K/28_4k.mp4",
"/animated-bgs/Thumbnails/29_Thumb.mp4": "/animated-bgs/4K/29_4k.mp4",
"/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4",
};
const handleMouseEnter = (index: number) => {
setHoveredVideo(index);
playVideo(index);
};
const handleMouseLeave = (index: number) => {
setHoveredVideo(null);
pauseVideo(index);
};
// Function to play the video
const playVideo = (index: number) => {
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
if (video) {
video.play();
}
};
// Function to pause the video
const pauseVideo = (index: number) => {
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
if (video) {
video.pause();
}
};
const handleBg = (x: string) => {
setColor(x);
handleBgChange(x, "animation");
};
return (
<div>
<div className="mt-4 grid grid-cols-6 gap-4">
{Object.keys(animationFiles).map((key, index) => {
const value = animationFiles[key];
return (
<div
key={index}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
onClick={() => handleBg(value)}
className="relative cursor-pointer overflow-hidden rounded-lg">
<video
disablePictureInPicture
id={`video-${index}`}
autoPlay={hoveredVideo === index}
className="h-46 w-96 origin-center scale-105 transform">
<source src={`${key}`} type="video/mp4" />
</video>
<input
className="absolute right-2 top-2 h-4 w-4 rounded-sm bg-white "
type="checkbox"
checked={color === value}
onChange={() => handleBg(value)}
/>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { TSurvey } from "@formbricks/types/surveys";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { useState } from "react";
interface ColorSurveyBgBgProps {
localSurvey?: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
colours: string[];
}
export default function ColorSurveyBg({ localSurvey, handleBgChange, colours }: ColorSurveyBgBgProps) {
const [color, setColor] = useState(localSurvey?.styling?.background?.bg || "#ffff");
const handleBg = (x: string) => {
setColor(x);
handleBgChange(x, "color");
};
return (
<div>
<div className="w-full max-w-xs py-2">
<ColorPicker color={color} onChange={handleBg} />
</div>
<div className="grid grid-cols-4 gap-4 md:grid-cols-5 xl:grid-cols-8 2xl:grid-cols-10">
{colours.map((x) => {
return (
<div
className={`h-16 w-16 cursor-pointer rounded-lg ${
color === x ? "border-4 border-slate-500" : ""
}`}
key={x}
style={{ backgroundColor: `${x}` }}
onClick={() => handleBg(x)}></div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import FileInput from "@formbricks/ui/FileInput";
import { TSurvey } from "@formbricks/types/surveys";
interface ImageSurveyBgBgProps {
localSurvey?: TSurvey;
handleBgChange: (url: string, bgType: string) => void;
}
export default function ImageSurveyBg({ localSurvey, handleBgChange }: ImageSurveyBgBgProps) {
const isUrl = (str: string) => {
try {
new URL(str);
return true;
} catch (error) {
return false;
}
};
const fileUrl = isUrl(localSurvey?.styling?.background?.bg ?? "")
? localSurvey?.styling?.background?.bg ?? ""
: "";
return (
<div className="mb-2 mt-4 w-full rounded-lg border bg-slate-50 p-4">
<div className="flex w-full items-center justify-center">
<FileInput
id="survey-bg-file-input"
allowedFileExtensions={["png", "jpeg", "jpg"]}
environmentId={localSurvey?.environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {
handleBgChange(url[0], "image");
} else {
handleBgChange("#ffff", "color");
}
}}
fileUrl={fileUrl}
/>
</div>
</div>
);
}

View File

@@ -18,6 +18,7 @@ interface SettingsViewProps {
attributeClasses: TAttributeClass[];
responseCount: number;
membershipRole?: TMembershipRole;
colours: string[];
}
export default function SettingsView({
@@ -28,6 +29,7 @@ export default function SettingsView({
attributeClasses,
responseCount,
membershipRole,
colours,
}: SettingsViewProps) {
return (
<div className="mt-12 space-y-3 p-5">
@@ -60,7 +62,7 @@ export default function SettingsView({
environmentId={environment.id}
/>
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { TPlacement } from "@formbricks/types/common";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurvey, TSurveyBackgroundBgType } from "@formbricks/types/surveys";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
@@ -9,18 +9,28 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import Placement from "./Placement";
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
interface StylingCardProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
colours: string[];
}
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
export default function StylingCard({ localSurvey, setLocalSurvey, colours }: StylingCardProps) {
const [open, setOpen] = useState(false);
const { type, productOverwrites } = localSurvey;
const { type, productOverwrites, styling } = localSurvey;
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
productOverwrites ?? {};
const { bg, bgType, brightness } = styling?.background ?? {};
const [inputValue, setInputValue] = useState(100);
const handleInputChange = (e) => {
setInputValue(e.target.value);
handleBrightnessChange(parseInt(e.target.value));
};
const togglePlacement = () => {
setLocalSurvey({
@@ -42,6 +52,34 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
});
};
const toggleBackgroundColor = () => {
setLocalSurvey({
...localSurvey,
styling: {
...localSurvey.styling,
background: {
...localSurvey.styling?.background,
bg: !!bg ? undefined : "#ffff",
bgType: !!bg ? undefined : "color",
},
},
});
};
const toggleBrightness = () => {
setLocalSurvey({
...localSurvey,
styling: {
...localSurvey.styling,
background: {
...localSurvey.styling?.background,
brightness: !!brightness ? undefined : 100,
},
},
});
setInputValue(100);
};
const toggleHighlightBorderColor = () => {
setLocalSurvey({
...localSurvey,
@@ -62,6 +100,35 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
});
};
const handleBgChange = (color: string, type: TSurveyBackgroundBgType) => {
setInputValue(100);
setLocalSurvey({
...localSurvey,
styling: {
...localSurvey.styling,
background: {
...localSurvey.styling?.background,
bg: color,
bgType: type,
brightness: undefined,
},
},
});
};
const handleBrightnessChange = (percent: number) => {
setLocalSurvey({
...localSurvey,
styling: {
...(localSurvey.styling || {}),
background: {
...localSurvey.styling?.background,
brightness: percent,
},
},
});
};
const handleBorderColorChange = (color: string) => {
setLocalSurvey({
...localSurvey,
@@ -143,6 +210,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
</div>
)}
</div>
{type == "link" && (
<>
{/* Background */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch id="autoCompleteBg" checked={!!bg} onCheckedChange={toggleBackgroundColor} />
<Label htmlFor="autoCompleteBg" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
<p className="text-xs font-normal text-slate-500">
Pick a background from our library or upload your own.
</p>
</div>
</Label>
</div>
{bg && (
<SurveyBgSelectorTab
localSurvey={localSurvey}
handleBgChange={handleBgChange}
colours={colours}
bgType={bgType}
/>
)}
</div>
{/* Overlay */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch
id="autoCompleteOverlay"
checked={!!brightness}
onCheckedChange={toggleBrightness}
/>
<Label htmlFor="autoCompleteOverlay" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
<p className="text-xs font-normal text-slate-500">
Darken or lighten background of your choice.
</p>
</div>
</Label>
</div>
{brightness && (
<div>
<div className="mt-4 flex flex-col justify-center rounded-lg border bg-slate-50 p-4 px-8">
<h3 className="mb-4 text-sm font-semibold text-slate-700">Transparency</h3>
<input
id="small-range"
type="range"
min="1"
max="200"
value={inputValue}
onChange={handleInputChange}
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</div>
</div>
)}
</div>
</>
)}
{/* positioning */}
{type !== "link" && (
<div className="p-3 ">

View File

@@ -0,0 +1,61 @@
import { TSurvey } from "@formbricks/types/surveys";
import { useState } from "react";
import AnimatedSurveyBg from "./AnimatedSurveyBg";
import ColorSurveyBg from "./ColorSurveyBg";
import ImageSurveyBg from "./ImageSurveyBg";
interface SurveyBgSelectorTabProps {
localSurvey: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
colours: string[];
bgType: string | null | undefined;
}
const TabButton = ({ isActive, onClick, children }) => (
<button
className={`w-1/4 rounded-md p-2 text-sm font-medium leading-none text-slate-800 ${
isActive ? "bg-white shadow-sm" : ""
}`}
onClick={onClick}>
{children}
</button>
);
export default function SurveyBgSelectorTab({
localSurvey,
handleBgChange,
colours,
bgType,
}: SurveyBgSelectorTabProps) {
const [tab, setTab] = useState(bgType || "image");
const renderContent = () => {
switch (tab) {
case "image":
return <ImageSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
case "animation":
return <AnimatedSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
case "color":
return <ColorSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} colours={colours} />;
default:
return null;
}
};
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg border bg-slate-50 p-4 px-8">
<div className="flex w-full items-center justify-between rounded-lg border border-slate-300 bg-slate-50 px-6 py-1.5">
<TabButton isActive={tab === "image"} onClick={() => setTab("image")}>
Image
</TabButton>
<TabButton isActive={tab === "animation"} onClick={() => setTab("animation")}>
Animation
</TabButton>
<TabButton isActive={tab === "color"} onClick={() => setTab("color")}>
Color
</TabButton>
</div>
{renderContent()}
</div>
);
}

View File

@@ -23,6 +23,7 @@ interface SurveyEditorProps {
attributeClasses: TAttributeClass[];
responseCount: number;
membershipRole?: TMembershipRole;
colours: string[];
}
export default function SurveyEditor({
@@ -33,6 +34,7 @@ export default function SurveyEditor({
attributeClasses,
responseCount,
membershipRole,
colours,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -99,6 +101,7 @@ export default function SurveyEditor({
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={membershipRole}
colours={colours}
/>
)}
</main>

View File

@@ -222,6 +222,11 @@ export default function SurveyMenuBar({
const { isDraft, ...rest } = question;
return rest;
}),
attributeFilters: localSurvey.attributeFilters.filter((attributeFilter) => {
if (attributeFilter.attributeClassId && attributeFilter.value) {
return true;
}
}),
};
if (!validateSurvey(localSurvey)) {
@@ -252,6 +257,14 @@ export default function SurveyMenuBar({
}
};
function containsEmptyTriggers() {
return (
localSurvey.type === "web" &&
localSurvey.triggers &&
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0)
);
}
return (
<>
{environment?.type === "development" && (
@@ -298,7 +311,7 @@ export default function SurveyMenuBar({
/>
</div>
<Button
disabled={isSurveyPublishing}
disabled={isSurveyPublishing || containsEmptyTriggers()}
variant={localSurvey.status === "draft" ? "secondary" : "darkCTA"}
className="mr-3"
loading={isSurveySaving}
@@ -318,11 +331,7 @@ export default function SurveyMenuBar({
)}
{localSurvey.status === "draft" && !audiencePrompt && (
<Button
disabled={
localSurvey.type === "web" &&
localSurvey.triggers &&
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0 || isSurveySaving)
}
disabled={isSurveySaving || containsEmptyTriggers()}
variant="darkCTA"
loading={isSurveyPublishing}
onClick={async () => {

View File

@@ -12,6 +12,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { getServerSession } from "next-auth";
import { colours } from "@formbricks/lib/constants";
import SurveyEditor from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
@@ -67,6 +68,7 @@ export default async function SurveysEditPage({ params }) {
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
colours={colours}
/>
</>
);

View File

@@ -56,7 +56,7 @@ export default function Modal({
ref={modalRef}
style={highlightBorderColorStyle}
className={cn(
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out ",
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full ",
slidingAnimationClass
)}>

View File

@@ -2,7 +2,7 @@
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import { TUploadFileConfig } from "@formbricks/types/storage";
@@ -155,6 +155,20 @@ export default function PreviewSurvey({
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
}
function animationTrigger() {
let storePreviewMode = previewMode;
setPreviewMode("null");
setTimeout(() => {
setPreviewMode(storePreviewMode);
}, 10);
}
useEffect(() => {
if (survey.styling?.background?.bgType === "animation") {
animationTrigger();
}
}, [survey.styling?.background?.bg]);
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
@@ -194,7 +208,7 @@ export default function PreviewSurvey({
<div className="absolute right-0 top-0 m-2">
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
</div>
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500 bg-slate-400">
<MediaBackground survey={survey} ContentRef={ContentRef} isMobilePreview>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-0 z-20 h-4 w-1/2 -translate-x-1/2 transform rounded-b-md bg-slate-500"></div>
{previewType === "modal" ? (
@@ -214,25 +228,19 @@ export default function PreviewSurvey({
/>
</Modal>
) : (
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
ref={ContentRef}>
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white py-6">
<div className="w-full max-w-md px-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
onFileUpload={onFileUpload}
responseCount={42}
/>
</div>
</div>
<div className="relative z-10 w-full max-w-md px-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
onFileUpload={onFileUpload}
responseCount={42}
/>
</div>
)}
</div>
</MediaBackground>
</>
)}
{previewMode === "desktop" && (
@@ -287,22 +295,20 @@ export default function PreviewSurvey({
/>
</Modal>
) : (
<div className="flex flex-grow flex-col overflow-y-auto rounded-b-lg" ref={ContentRef}>
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white p-4 py-6">
<div className="w-full max-w-md">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
responseCount={42}
/>
</div>
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
<div className="z-0 w-full max-w-md rounded-lg p-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
responseCount={42}
/>
</div>
</div>
</MediaBackground>
)}
</div>
)}

View File

@@ -2526,4 +2526,5 @@ export const minimalSurvey: TSurvey = {
},
productOverwrites: null,
singleUse: null,
styling: null,
};

View File

@@ -1,19 +1,28 @@
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
import Link from "next/link";
export default function LegalFooter() {
interface LegalFooterProps {
bgColor?: string | null;
}
export default function LegalFooter({ bgColor }: LegalFooterProps) {
if (!IMPRINT_URL && !PRIVACY_URL) return null;
return (
<div className="h-10 w-full border-t border-slate-200">
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
<div
className={`fixed bottom-0 h-12 w-full`}
style={{
backgroundColor: `${bgColor}`,
}}>
<div className="mx-auto max-w-lg p-3 text-center text-xs text-slate-400">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank">
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
Imprint
</Link>
)}
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
{PRIVACY_URL && (
<Link href={PRIVACY_URL} target="_blank">
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
Privacy Policy
</Link>
)}

View File

@@ -1,20 +1,20 @@
"use client";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import { SurveyInline } from "@formbricks/ui/Survey";
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
import { FormbricksAPI } from "@formbricks/api";
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import { SurveyState } from "@formbricks/lib/surveyState";
import { TProduct } from "@formbricks/types/product";
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey } from "@formbricks/types/surveys";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import { SurveyInline } from "@formbricks/ui/Survey";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { FormbricksAPI } from "@formbricks/api";
import { TUploadFileConfig } from "@formbricks/types/storage";
interface LinkSurveyProps {
survey: TSurvey;
@@ -119,7 +119,7 @@ export default function LinkSurvey({
return (
<>
<ContentWrapper className="h-full w-full p-0 md:max-w-lg">
<ContentWrapper className="h-full w-full p-0 md:max-w-md">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />

View File

@@ -0,0 +1,87 @@
"use client";
import { TSurvey } from "@formbricks/types/surveys";
import React from "react";
interface MediaBackgroundProps {
children: React.ReactNode;
survey: TSurvey;
isEditorView?: boolean;
isMobilePreview?: boolean;
ContentRef?: React.RefObject<HTMLDivElement>;
}
export const MediaBackground: React.FC<MediaBackgroundProps> = ({
children,
survey,
isEditorView = false,
isMobilePreview = false,
ContentRef,
}) => {
const getFilterStyle = () => {
return survey.styling?.background?.brightness
? `brightness-${survey.styling?.background?.brightness}`
: "";
};
const renderBackground = () => {
const filterStyle = getFilterStyle();
const baseClasses = "absolute inset-0 h-full w-full";
switch (survey.styling?.background?.bgType) {
case "color":
return (
<div
className={`${baseClasses} ${filterStyle}`}
style={{ backgroundColor: survey.styling?.background?.bg || "#ffff" }}
/>
);
case "animation":
return (
<video muted loop autoPlay className={`${baseClasses} object-cover ${filterStyle}`}>
<source src={survey.styling?.background?.bg || ""} type="video/mp4" />
</video>
);
case "image":
return (
<div
className={`${baseClasses} bg-cover bg-center ${filterStyle}`}
style={{ backgroundImage: `url(${survey.styling?.background?.bg})` }}
/>
);
default:
return <div className={`${baseClasses} bg-white`} />;
}
};
const renderContent = () => (
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">{children}</div>
);
if (isMobilePreview) {
return (
<div
ref={ContentRef}
className={`relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-3xl border-8 border-slate-500 ${getFilterStyle()}`}>
{renderBackground()}
{renderContent()}
</div>
);
} else if (isEditorView) {
return (
<div ref={ContentRef} className="flex flex-grow flex-col overflow-y-auto rounded-b-lg">
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
{renderBackground()}
<div className="flex h-full w-full items-center justify-center">{children}</div>
</div>
</div>
);
} else {
return (
<div className="flex min-h-screen flex-col items-center justify-center px-2">
{renderBackground()}
<div className="relative w-full">{children}</div>
</div>
);
}
};

View File

@@ -1,15 +1,15 @@
"use client";
import type { NextPage } from "next";
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
import { TResponse } from "@formbricks/types/responses";
import { OTPInput } from "@formbricks/ui/OTPInput";
import { useCallback, useEffect, useState } from "react";
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { cn } from "@formbricks/lib/cn";
import { OTPInput } from "@formbricks/ui/OTPInput";
import type { NextPage } from "next";
import { useCallback, useEffect, useState } from "react";
interface LinkSurveyPinScreenProps {
surveyId: string;

View File

@@ -1,10 +1,3 @@
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
export default async function SurveyLayout({ children }) {
return (
<div className="flex h-full flex-col justify-between bg-white">
<div className="h-full overflow-y-auto">{children}</div>
<LegalFooter />
</div>
);
return <div>{children}</div>;
}

View File

@@ -1,7 +1,9 @@
export const revalidate = REVALIDATION_INTERVAL;
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
@@ -12,6 +14,7 @@ import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { TResponse } from "@formbricks/types/responses";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getEmailVerificationStatus } from "./lib/helpers";
import { ZId } from "@formbricks/types/environment";
@@ -183,17 +186,22 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
);
}
return (
<LinkSurvey
survey={survey}
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
responseCount={survey.welcomeCard.showResponseCount ? responseCount : undefined}
/>
);
return survey ? (
<div>
<MediaBackground survey={survey}>
<LinkSurvey
survey={survey}
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
responseCount={survey.welcomeCard.showResponseCount ? responseCount : undefined}
/>
</MediaBackground>
<LegalFooter bgColor={survey.styling?.background?.bg || "#ffff"} />
</div>
) : null;
}

View File

@@ -59,24 +59,17 @@ x-github-auth-enabled: &github_auth_enabled 0
x-github-id: &github_id
x-github-secret: &github_secret # Configure Google Login
x-google-auth-enabled: &google_auth_enabled 0
x-google-client-id: &google_client_id
x-google-client-secret: &google_client_secret # Disable Sentry warning
x-sentry-ignore-api-resolution-error: &sentry_ignore_api_resolution_error # Enable Sentry Error Tracking
x-next-public-sentry-dsn: &next_public_sentry_dsn # Cron Secret
# Set this to a random string to secure your cron endpoints
x-cron-secret: &cron_secret YOUR_CRON_SECRET
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
x-asset-prefix-url: &asset_prefix_url
services:
postgres:
restart: always
@@ -130,7 +123,6 @@ services:
GOOGLE_CLIENT_ID: *google_client_id
GOOGLE_CLIENT_SECRET: *google_client_secret
CRON_SECRET: *cron_secret
ASSET_PREFIX_URL: *asset_prefix_url
volumes:
postgres:

View File

@@ -65,9 +65,6 @@ x-environment: &environment
# GOOGLE_CLIENT_ID:
# GOOGLE_CLIENT_SECRET:
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
# ASSET_PREFIX_URL: *asset_prefix_url
services:
postgres:
restart: always

View File

@@ -30,6 +30,8 @@
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@playwright/test": "^1.40.1",
"@types/node": "20.10.1",
"eslint-config-formbricks": "workspace:*",
"husky": "^8.0.3",
"lint-staged": "^15.1.0",
@@ -52,11 +54,14 @@
"engines": {
"node": ">=16.0.0"
},
"packageManager": "pnpm@8.1.1",
"packageManager": "pnpm@8.11.0",
"nextBundleAnalysis": {
"budget": 358400,
"budgetPercentIncreaseRed": 20,
"minimumChangeThreshold": 0,
"showDetails": true
},
"dependencies": {
"playwright": "^1.40.1"
}
}

View File

@@ -6,6 +6,7 @@ import {
TSurveyClosedMessage,
TSurveyHiddenFields,
TSurveyProductOverwrites,
TSurveyStyling,
TSurveyQuestions,
TSurveySingleUse,
TSurveyThankYouCard,
@@ -27,6 +28,7 @@ declare global {
export type SurveyThankYouCard = TSurveyThankYouCard;
export type SurveyHiddenFields = TSurveyHiddenFields;
export type SurveyProductOverwrites = TSurveyProductOverwrites;
export type SurveyStyling = TSurveyStyling;
export type SurveyClosedMessage = TSurveyClosedMessage;
export type SurveySingleUse = TSurveySingleUse;
export type SurveyVerifyEmail = TSurveyVerifyEmail;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "styling" JSONB;

View File

@@ -289,6 +289,9 @@ model Survey {
/// @zod.custom(imports.ZSurveyProductOverwrites)
/// [SurveyProductOverwrites]
productOverwrites Json?
/// @zod.custom(imports.ZSurveyStyling)
/// [SurveyStyling]
styling Json?
/// @zod.custom(imports.ZSurveySingleUse)
/// [SurveySingleUse]
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")

View File

@@ -18,6 +18,7 @@ export {
ZSurveyHiddenFields,
ZSurveyClosedMessage,
ZSurveyProductOverwrites,
ZSurveyStyling,
ZSurveyVerifyEmail,
ZSurveySingleUse,
} from "@formbricks/types/surveys";

View File

@@ -154,7 +154,7 @@ export const authOptions: NextAuthOptions = {
async signIn({ user, account }: any) {
if (account.provider === "credentials" || account.provider === "token") {
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
throw new Error("Email Verification is Pending");
}
return true;
}
@@ -197,7 +197,9 @@ export const authOptions: NextAuthOptions = {
await updateProfile(existingUserWithAccount.id, { email: user.email });
return true;
}
return "/auth/login?error=Looks%20like%20you%20updated%20your%20email%20somewhere%20else.%0AA%20user%20with%20this%20new%20email%20exists%20already.";
throw new Error(
"Looks like you updated your email somewhere else. A user with this new email exists already."
);
}
// There is no existing account for this identity provider / account id
@@ -206,7 +208,7 @@ export const authOptions: NextAuthOptions = {
const existingUserWithEmail = await getProfileByEmail(user.email);
if (existingUserWithEmail) {
return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already.";
throw new Error("A user with this email exists already.");
}
const userProfile = await createProfile({

View File

@@ -71,6 +71,34 @@ export const IS_S3_CONFIGURED: boolean =
export const PRICING_USERTARGETING_FREE_MTU = 2500;
export const PRICING_APPSURVEYS_FREE_RESPONSES = 250;
// Colors for Survey Bg
export const colours = [
"#FFF2D8",
"#EAD7BB",
"#BCA37F",
"#113946",
"#04364A",
"#176B87",
"#64CCC5",
"#DAFFFB",
"#132043",
"#1F4172",
"#F1B4BB",
"#FDF0F0",
"#001524",
"#445D48",
"#D6CC99",
"#FDE5D4",
"#BEADFA",
"#D0BFFF",
"#DFCCFB",
"#FFF8C9",
"#FF8080",
"#FFCF96",
"#F6FDC3",
"#CDFAD5",
];
// Rate Limiting
export const SIGNUP_RATE_LIMIT = {
interval: 60 * 60 * 1000, // 60 minutes

View File

@@ -270,14 +270,15 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
if (responseInput.personId) {
person = await getPerson(responseInput.personId);
}
const ttcTemp = responseInput.ttc;
const ttcTemp = responseInput.ttc ?? {};
const questionId = Object.keys(ttcTemp)[0];
const ttc = responseInput.finished
? {
...ttcTemp,
_total: ttcTemp[questionId], // Add _total property with the same value
}
: ttcTemp;
const ttc =
responseInput.finished && responseInput.ttc
? {
...ttcTemp,
_total: ttcTemp[questionId], // Add _total property with the same value
}
: ttcTemp;
const responsePrisma = await prisma.response.create({
data: {
survey: {
@@ -512,7 +513,11 @@ export const updateResponse = async (
...currentResponse.data,
...responseInput.data,
};
const ttc = responseInput.finished ? calculateTtcTotal(responseInput.ttc) : responseInput.ttc;
const ttc = responseInput.ttc
? responseInput.finished
? calculateTtcTotal(responseInput.ttc)
: responseInput.ttc
: {};
const responsePrisma = await prisma.response.update({
where: {

View File

@@ -14,6 +14,7 @@ import { getAttributeClasses } from "../attributeClass/service";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { displayCache } from "../display/cache";
import { getDisplaysByPersonId } from "../display/service";
import { personCache } from "../person/cache";
import { productCache } from "../product/cache";
import { getProductByEnvironmentId } from "../product/service";
import { responseCache } from "../response/cache";
@@ -44,6 +45,7 @@ export const selectSurvey = {
verifyEmail: true,
redirectUrl: true,
productOverwrites: true,
styling: true,
surveyClosedMessage: true,
singleUse: true,
pin: true,
@@ -540,6 +542,7 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp
};
export const duplicateSurvey = async (environmentId: string, surveyId: string) => {
validateInputs([environmentId, ZId], [surveyId, ZId]);
const existingSurvey = await getSurvey(surveyId);
if (!existingSurvey) {
@@ -585,6 +588,7 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
productOverwrites: existingSurvey.productOverwrites
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
: Prisma.JsonNull,
styling: existingSurvey.styling ? JSON.parse(JSON.stringify(existingSurvey.styling)) : Prisma.JsonNull,
verifyEmail: existingSurvey.verifyEmail
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
: Prisma.JsonNull,
@@ -605,8 +609,10 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
return newSurvey;
};
export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<TSurvey[]> =>
unstable_cache(
export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<TSurvey[]> => {
validateInputs([environmentId, ZId]);
return unstable_cache(
async () => {
const product = await getProductByEnvironmentId(environmentId);
@@ -685,9 +691,10 @@ export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<
return surveys;
},
[`getSyncSurveys-${environmentId}`],
[`getSyncSurveys-${environmentId}-${person.userId}`],
{
tags: [
personCache.tag.byEnvironmentIdAndUserId(environmentId, person.userId),
displayCache.tag.byPersonId(person.id),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
@@ -695,3 +702,4 @@ export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
};

View File

@@ -13,11 +13,10 @@ export default function Headline({
}: HeadlineProps) {
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" : "mr-[3ch] justify-between"}`}>
<div className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}>
{headline}
{!required && (
<span className="text-info-text self-start text-sm font-normal leading-7" tabIndex={-1}>
<span className="text-info-text ml-2 self-start text-sm font-normal leading-7" tabIndex={-1}>
Optional
</span>
)}

View File

@@ -174,7 +174,7 @@ export function Survey({
return (
<>
<AutoCloseWrapper survey={survey} onClose={onClose}>
<div className="flex h-full w-full flex-col justify-between bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
<div className="flex h-full w-full flex-col justify-between rounded-lg bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
// Handle the case when there are no questions and both welcome and thank you cards are disabled

View File

@@ -37,11 +37,13 @@ export type TResponseNote = z.infer<typeof ZResponseNote>;
export const ZResponseMeta = z.object({
source: z.string().optional(),
url: z.string().optional(),
userAgent: z.object({
browser: z.string().optional(),
os: z.string().optional(),
device: z.string().optional(),
}),
userAgent: z
.object({
browser: z.string().optional(),
os: z.string().optional(),
device: z.string().optional(),
})
.optional(),
});
export type TResponseMeta = z.infer<typeof ZResponseMeta>;
@@ -55,7 +57,7 @@ export const ZResponse = z.object({
personAttributes: ZResponsePersonAttributes,
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
ttc: ZResponseTtc.optional(),
notes: z.array(ZResponseNote),
tags: z.array(ZTag),
meta: ZResponseMeta.nullable(),
@@ -77,7 +79,7 @@ export const ZResponseInput = z.object({
singleUseId: z.string().nullable().optional(),
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
ttc: ZResponseTtc.optional(),
meta: z
.object({
source: z.string().optional(),
@@ -104,7 +106,7 @@ export type TResponseLegacyInput = z.infer<typeof ZResponseLegacyInput>;
export const ZResponseUpdateInput = z.object({
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
ttc: ZResponseTtc.optional(),
});
export type TResponseUpdateInput = z.infer<typeof ZResponseUpdateInput>;
@@ -118,7 +120,7 @@ export type TResponseWithSurvey = z.infer<typeof ZResponseWithSurvey>;
export const ZResponseUpdate = z.object({
finished: z.boolean(),
data: ZResponseData,
ttc: ZResponseTtc,
ttc: ZResponseTtc.optional(),
meta: z
.object({
url: z.string().optional(),

View File

@@ -45,6 +45,24 @@ export const ZSurveyProductOverwrites = z.object({
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
export const ZSurveyBackgroundBgType = z.enum(["animation", "color", "image"]);
export type TSurveyBackgroundBgType = z.infer<typeof ZSurveyBackgroundBgType>;
export const ZSurveyStylingBackground = z.object({
bg: z.string().nullish(),
bgType: z.enum(["animation", "color", "image"]).nullish(),
brightness: z.number().nullish(),
});
export type TSurveyStylingBackground = z.infer<typeof ZSurveyStylingBackground>;
export const ZSurveyStyling = z.object({
background: ZSurveyStylingBackground.nullish(),
});
export type TSurveyStyling = z.infer<typeof ZSurveyStyling>;
export const ZSurveyClosedMessage = z
.object({
enabled: z.boolean().optional(),
@@ -379,6 +397,7 @@ export const ZSurvey = z.object({
autoComplete: z.number().nullable(),
closeOnDate: z.date().nullable(),
productOverwrites: ZSurveyProductOverwrites.nullable(),
styling: ZSurveyStyling.nullable(),
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
singleUse: ZSurveySingleUse.nullable(),
verifyEmail: ZSurveyVerifyEmail.nullable(),

77
playwright.config.ts Normal file
View File

@@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "pnpm go",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});

147
pnpm-lock.yaml generated
View File

@@ -7,10 +7,20 @@ settings:
importers:
.:
dependencies:
playwright:
specifier: ^1.40.1
version: 1.40.1
devDependencies:
'@changesets/cli':
specifier: ^2.26.2
version: 2.26.2
'@playwright/test':
specifier: ^1.40.1
version: 1.40.1
'@types/node':
specifier: 20.10.1
version: 20.10.1
eslint-config-formbricks:
specifier: workspace:*
version: link:packages/eslint-config-formbricks
@@ -277,7 +287,7 @@ importers:
version: 7.2.0(typescript@5.3.2)
vite:
specifier: ^4.4.11
version: 4.5.0
version: 4.5.0(@types/node@20.10.1)
apps/web:
dependencies:
@@ -443,16 +453,16 @@ importers:
version: 9.0.0(eslint@8.54.0)
eslint-config-turbo:
specifier: latest
version: 1.8.8(eslint@8.54.0)
version: 1.10.16(eslint@8.54.0)
terser:
specifier: ^5.24.0
version: 5.24.0
vite:
specifier: ^5.0.4
version: 5.0.4(terser@5.24.0)
version: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
vite-plugin-dts:
specifier: ^3.6.4
version: 3.6.4(typescript@5.3.2)(vite@5.0.4)
version: 3.6.4(@types/node@20.10.1)(typescript@5.3.2)(vite@5.0.4)
packages/database:
dependencies:
@@ -526,7 +536,7 @@ importers:
version: 9.0.0(eslint@8.54.0)
eslint-config-turbo:
specifier: latest
version: 1.8.8(eslint@8.54.0)
version: 1.10.16(eslint@8.54.0)
eslint-plugin-react:
specifier: 7.33.2
version: 7.33.2(eslint@8.54.0)
@@ -586,13 +596,13 @@ importers:
version: 9.0.0(eslint@8.54.0)
eslint-config-turbo:
specifier: latest
version: 1.8.8(eslint@8.54.0)
version: 1.10.16(eslint@8.54.0)
isomorphic-fetch:
specifier: ^3.0.0
version: 3.0.0
jest:
specifier: ^29.7.0
version: 29.7.0
version: 29.7.0(@types/node@20.10.1)
jest-environment-jsdom:
specifier: ^29.7.0
version: 29.7.0
@@ -604,10 +614,10 @@ importers:
version: 5.24.0
vite:
specifier: ^5.0.4
version: 5.0.4(terser@5.24.0)
version: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
vite-plugin-dts:
specifier: ^3.6.4
version: 3.6.4(typescript@5.3.2)(vite@5.0.4)
version: 3.6.4(@types/node@20.10.1)(typescript@5.3.2)(vite@5.0.4)
packages/lib:
dependencies:
@@ -713,7 +723,7 @@ importers:
version: 9.0.0(eslint@8.54.0)
eslint-config-turbo:
specifier: latest
version: 1.8.8(eslint@8.54.0)
version: 1.10.16(eslint@8.54.0)
postcss:
specifier: ^8.4.31
version: 8.4.31
@@ -728,10 +738,10 @@ importers:
version: 5.24.0
vite:
specifier: ^5.0.4
version: 5.0.4(terser@5.24.0)
version: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
vite-plugin-dts:
specifier: ^3.6.4
version: 3.6.4(typescript@5.3.2)(vite@5.0.4)
version: 3.6.4(@types/node@20.10.1)(typescript@5.3.2)(vite@5.0.4)
vite-tsconfig-paths:
specifier: ^4.2.1
version: 4.2.1(typescript@5.3.2)(vite@5.0.4)
@@ -4294,7 +4304,7 @@ packages:
magic-string: 0.27.0
react-docgen-typescript: 2.2.2(typescript@5.3.2)
typescript: 5.3.2
vite: 4.5.0
vite: 4.5.0(@types/node@20.10.1)
dev: true
/@jridgewell/gen-mapping@0.3.3:
@@ -4668,24 +4678,24 @@ packages:
'@types/react': 18.2.28
react: 18.2.0
/@microsoft/api-extractor-model@7.28.2:
/@microsoft/api-extractor-model@7.28.2(@types/node@20.10.1):
resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==}
dependencies:
'@microsoft/tsdoc': 0.14.2
'@microsoft/tsdoc-config': 0.16.2
'@rushstack/node-core-library': 3.61.0
'@rushstack/node-core-library': 3.61.0(@types/node@20.10.1)
transitivePeerDependencies:
- '@types/node'
dev: true
/@microsoft/api-extractor@7.38.0:
/@microsoft/api-extractor@7.38.0(@types/node@20.10.1):
resolution: {integrity: sha512-e1LhZYnfw+JEebuY2bzhw0imDCl1nwjSThTrQqBXl40hrVo6xm3j/1EpUr89QyzgjqmAwek2ZkIVZbrhaR+cqg==}
hasBin: true
dependencies:
'@microsoft/api-extractor-model': 7.28.2
'@microsoft/api-extractor-model': 7.28.2(@types/node@20.10.1)
'@microsoft/tsdoc': 0.14.2
'@microsoft/tsdoc-config': 0.16.2
'@rushstack/node-core-library': 3.61.0
'@rushstack/node-core-library': 3.61.0(@types/node@20.10.1)
'@rushstack/rig-package': 0.5.1
'@rushstack/ts-command-line': 4.16.1
colors: 1.2.5
@@ -5239,6 +5249,14 @@ packages:
requiresBuild: true
optional: true
/@playwright/test@1.40.1:
resolution: {integrity: sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright: 1.40.1
dev: true
/@preact/preset-vite@2.7.0(@babel/core@7.23.5)(preact@10.19.2)(vite@5.0.4):
resolution: {integrity: sha512-m5N0FVtxbCCDxNk55NGhsRpKJChYcupcuQHzMJc/Bll07IKZKn8amwYciyKFS9haU6AgzDAJ/ewvApr6Qg1DHw==}
peerDependencies:
@@ -5254,7 +5272,7 @@ packages:
debug: 4.3.4
kolorist: 1.8.0
resolve: 1.22.8
vite: 5.0.4(terser@5.24.0)
vite: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
transitivePeerDependencies:
- preact
- supports-color
@@ -5288,7 +5306,7 @@ packages:
'@prefresh/utils': 1.2.0
'@rollup/pluginutils': 4.2.1
preact: 10.19.2
vite: 5.0.4(terser@5.24.0)
vite: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
transitivePeerDependencies:
- supports-color
dev: true
@@ -7030,7 +7048,7 @@ packages:
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
dev: true
/@rushstack/node-core-library@3.61.0:
/@rushstack/node-core-library@3.61.0(@types/node@20.10.1):
resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==}
peerDependencies:
'@types/node': '*'
@@ -7038,6 +7056,7 @@ packages:
'@types/node':
optional: true
dependencies:
'@types/node': 20.10.1
colors: 1.2.5
fs-extra: 7.0.1
import-lazy: 4.0.0
@@ -8150,7 +8169,7 @@ packages:
magic-string: 0.30.5
rollup: 3.29.4
typescript: 5.3.2
vite: 4.5.0
vite: 4.5.0(@types/node@20.10.1)
transitivePeerDependencies:
- encoding
- supports-color
@@ -8515,7 +8534,7 @@ packages:
react: 18.2.0
react-docgen: 6.0.4
react-dom: 18.2.0(react@18.2.0)
vite: 4.5.0
vite: 4.5.0(@types/node@20.10.1)
transitivePeerDependencies:
- '@preact/preset-vite'
- encoding
@@ -8968,7 +8987,7 @@ packages:
/@types/jsonwebtoken@9.0.5:
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
dependencies:
'@types/node': 20.9.0
'@types/node': 20.10.1
dev: true
/@types/keyv@3.1.4:
@@ -9052,18 +9071,6 @@ packages:
dependencies:
undici-types: 5.26.5
/@types/node@20.8.6:
resolution: {integrity: sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==}
dependencies:
undici-types: 5.25.3
dev: true
/@types/node@20.9.0:
resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==}
dependencies:
undici-types: 5.26.5
dev: true
/@types/normalize-package-data@2.4.3:
resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==}
@@ -9080,7 +9087,7 @@ packages:
/@types/qrcode@1.5.5:
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
dependencies:
'@types/node': 20.8.6
'@types/node': 20.10.1
dev: true
/@types/qs@6.9.9:
@@ -9647,7 +9654,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.5)
magic-string: 0.27.0
react-refresh: 0.14.0
vite: 4.5.0
vite: 4.5.0(@types/node@20.10.1)
transitivePeerDependencies:
- supports-color
dev: true
@@ -9663,7 +9670,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2)
'@types/babel__core': 7.20.3
react-refresh: 0.14.0
vite: 4.5.0
vite: 4.5.0(@types/node@20.10.1)
transitivePeerDependencies:
- supports-color
dev: true
@@ -11417,7 +11424,7 @@ packages:
capture-stack-trace: 1.0.2
dev: false
/create-jest@29.7.0:
/create-jest@29.7.0(@types/node@20.10.1):
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -12385,13 +12392,13 @@ packages:
eslint: 8.54.0
dev: true
/eslint-config-turbo@1.8.8(eslint@8.54.0):
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
/eslint-config-turbo@1.10.16(eslint@8.54.0):
resolution: {integrity: sha512-O3NQI72bQHV7FvSC6lWj66EGx8drJJjuT1kuInn6nbMLOHdMBhSUX/8uhTAlHRQdlxZk2j9HtgFCIzSc93w42g==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
eslint: 8.54.0
eslint-plugin-turbo: 1.8.8(eslint@8.54.0)
eslint-plugin-turbo: 1.10.16(eslint@8.54.0)
dev: true
/eslint-import-resolver-node@0.3.9:
@@ -12575,11 +12582,12 @@ packages:
- typescript
dev: true
/eslint-plugin-turbo@1.8.8(eslint@8.54.0):
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
/eslint-plugin-turbo@1.10.16(eslint@8.54.0):
resolution: {integrity: sha512-ZjrR88MTN64PNGufSEcM0tf+V1xFYVbeiMeuIqr0aiABGomxFLo4DBkQ7WI4WzkZtWQSIA2sP+yxqSboEfL9MQ==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
dotenv: 16.0.3
eslint: 8.54.0
dev: true
@@ -13234,6 +13242,13 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
optional: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -14702,7 +14717,7 @@ packages:
- supports-color
dev: true
/jest-cli@29.7.0:
/jest-cli@29.7.0(@types/node@20.10.1):
resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -14716,7 +14731,7 @@ packages:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0
create-jest: 29.7.0(@types/node@20.10.1)
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@20.10.1)
@@ -14811,7 +14826,7 @@ packages:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/jsdom': 20.0.1
'@types/node': 20.8.6
'@types/node': 20.10.1
jest-mock: 29.7.0
jest-util: 29.7.0
jsdom: 20.0.3
@@ -15102,7 +15117,7 @@ packages:
supports-color: 8.1.1
dev: true
/jest@29.7.0:
/jest@29.7.0(@types/node@20.10.1):
resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@@ -15115,7 +15130,7 @@ packages:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0
jest-cli: 29.7.0(@types/node@20.10.1)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -17624,6 +17639,20 @@ packages:
dependencies:
find-up: 5.0.0
/playwright-core@1.40.1:
resolution: {integrity: sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==}
engines: {node: '>=16'}
hasBin: true
/playwright@1.40.1:
resolution: {integrity: sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.40.1
optionalDependencies:
fsevents: 2.3.2
/pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
@@ -20578,10 +20607,6 @@ packages:
which-boxed-primitive: 1.0.2
dev: true
/undici-types@5.25.3:
resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==}
dev: true
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
@@ -21035,7 +21060,7 @@ packages:
vfile-message: 3.1.4
dev: false
/vite-plugin-dts@3.6.4(typescript@5.3.2)(vite@5.0.4):
/vite-plugin-dts@3.6.4(@types/node@20.10.1)(typescript@5.3.2)(vite@5.0.4):
resolution: {integrity: sha512-yOVhUI/kQhtS6lCXRYYLv2UUf9bftcwQK9ROxCX2ul17poLQs02ctWX7+vXB8GPRzH8VCK3jebEFtPqqijXx6w==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
@@ -21045,13 +21070,13 @@ packages:
vite:
optional: true
dependencies:
'@microsoft/api-extractor': 7.38.0
'@microsoft/api-extractor': 7.38.0(@types/node@20.10.1)
'@rollup/pluginutils': 5.0.5(rollup@2.78.0)
'@vue/language-core': 1.8.22(typescript@5.3.2)
debug: 4.3.4
kolorist: 1.8.0
typescript: 5.3.2
vite: 5.0.4(terser@5.24.0)
vite: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
vue-tsc: 1.8.22(typescript@5.3.2)
transitivePeerDependencies:
- '@types/node'
@@ -21070,13 +21095,13 @@ packages:
debug: 4.3.4
globrex: 0.1.2
tsconfck: 2.1.2(typescript@5.3.2)
vite: 5.0.4(terser@5.24.0)
vite: 5.0.4(@types/node@20.10.1)(terser@5.24.0)
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/vite@4.5.0:
/vite@4.5.0(@types/node@20.10.1):
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -21104,6 +21129,7 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 20.10.1
esbuild: 0.18.20
postcss: 8.4.31
rollup: 3.29.4
@@ -21111,7 +21137,7 @@ packages:
fsevents: 2.3.3
dev: true
/vite@5.0.4(terser@5.24.0):
/vite@5.0.4(@types/node@20.10.1)(terser@5.24.0):
resolution: {integrity: sha512-RzAr8LSvM8lmhB4tQ5OPcBhpjOZRZjuxv9zO5UcxeoY2bd3kP3Ticd40Qma9/BqZ8JS96Ll/jeBX9u+LJZrhVg==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@@ -21139,6 +21165,7 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 20.10.1
esbuild: 0.19.5
postcss: 8.4.31
rollup: 4.4.1

41
tests/initial.spec.ts Normal file
View File

@@ -0,0 +1,41 @@
import { test, expect } from "@playwright/test";
const email = "testp@gmail.com";
const password = "Test@123";
async function createUser(page) {
await page.goto("http://localhost:3000/auth/signup");
await page.getByText("Continue with Email").click();
await page.fill('input[name="name"]', "test");
await page.press('input[name="name"]', "Tab");
await page.fill('input[name="email"]', email);
await page.press('input[name="email"]', "Tab");
await page.fill('input[name="password"]', password);
await page.press('input[name="password"]', "Enter");
}
class LoginPage {
async login(page, email, password) {
await page.getByText("Login").click();
await page.getByText("Login with Email").click();
await page.fill('input[name="email"]', email);
await page.press('input[name="email"]', "Tab");
await page.fill('input[name="password"]', password);
await page.press('input[name="password"]', "Enter");
}
}
test("create account, login, and complete onboarding", async ({ page }) => {
const loginPage = new LoginPage();
await createUser(page);
await loginPage.login(page, email, password);
await completeOnboarding(page);
});
await expect(page).toHaveTitle(/Your Surveys | Formbricks/);
}
test("create account, login, and complete onboarding", async ({ page }) => {
await createUser(page);
await loginUser(page);
await completeOnboarding(page);
});