mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 19:39:01 -05:00
7eb94f0bd5
Co-authored-by: Johannes <johannes@formbricks.com>
379 lines
17 KiB
TypeScript
379 lines
17 KiB
TypeScript
import { expect } from "@playwright/test";
|
||
import { test } from "./lib/fixtures";
|
||
|
||
test.describe("Survey Styling", async () => {
|
||
// Shared Helpers
|
||
const openAccordion = async (page: any, name: string) => {
|
||
// Find the trigger by text, ensuring we get the visible one (important for nested or reusable components)
|
||
const accordionHeader = page
|
||
.locator("div,button")
|
||
.filter({ hasText: name })
|
||
.locator("visible=true")
|
||
.last();
|
||
// Check if open (aria-expanded or data-state)
|
||
const expanded = await accordionHeader.getAttribute("aria-expanded");
|
||
const state = await accordionHeader.getAttribute("data-state");
|
||
if (expanded === "false" || state === "closed" || (!expanded && !state)) {
|
||
await accordionHeader.click();
|
||
await page.waitForTimeout(500); // Animation
|
||
}
|
||
};
|
||
|
||
const setColor = async (page: any, label: string, hex: string) => {
|
||
const labelEl = page.locator("label").filter({ hasText: label }).locator("visible=true").last();
|
||
const container = labelEl.locator("..");
|
||
await container.getByRole("textbox").fill(hex.replace("#", ""));
|
||
await container.getByRole("textbox").blur();
|
||
};
|
||
|
||
const setDimension = async (page: any, label: string, value: string) => {
|
||
const labelEl = page.locator("label").filter({ hasText: label }).locator("visible=true").last();
|
||
const container = labelEl.locator("..");
|
||
// Try finding number input, fallback to simple fill if it's text
|
||
const input = container.locator('input[type="number"], input[type="text"]').first();
|
||
await input.fill(value);
|
||
await input.blur();
|
||
};
|
||
|
||
test("Global Theme Styling (Workspace Level)", async ({ page, users }) => {
|
||
const user = await users.create();
|
||
await user.login();
|
||
|
||
// Navigate to Look & Feel settings
|
||
await page.getByRole("link", { name: "Configuration" }).click();
|
||
await page.getByRole("link", { name: "Look & Feel" }).click();
|
||
await page.waitForURL(/\/environments\/[^/]+\/workspace\/look/);
|
||
|
||
// Toggle "Enable custom styling"
|
||
const addCustomStyles = page.getByLabel("Enable custom styling");
|
||
if (!(await addCustomStyles.isChecked())) {
|
||
await addCustomStyles.click();
|
||
}
|
||
|
||
// --- Survey styling ---
|
||
await openAccordion(page, "Survey styling");
|
||
|
||
// 1. Headlines & Descriptions
|
||
await openAccordion(page, "Headlines & Descriptions");
|
||
await setColor(page, "Headline Color", "aa0000"); // Red-ish
|
||
await setColor(page, "Description Color", "00aa00"); // Green-ish
|
||
await setDimension(page, "Headline Font Size", "24");
|
||
await setDimension(page, "Description Font Size", "18");
|
||
await setDimension(page, "Headline Font Weight", "700");
|
||
await setColor(page, "Headline Label Color", "0000aa"); // Blue-ish
|
||
await setDimension(page, "Headline Label Font Size", "14");
|
||
await setDimension(page, "Headline Label Font Weight", "600");
|
||
|
||
// Verify Typography Variables
|
||
await page.waitForTimeout(1000);
|
||
let css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toContain("--fb-element-headline-color: #aa0000");
|
||
expect(css).toContain("--fb-element-description-color: #00aa00");
|
||
expect(css).toContain("--fb-element-headline-font-size: 24px");
|
||
expect(css).toContain("--fb-element-description-font-size: 18px");
|
||
expect(css).toContain("--fb-element-headline-font-weight: 700");
|
||
expect(css).toContain("--fb-element-upper-label-color: #0000aa");
|
||
expect(css).toContain("--fb-element-upper-label-font-size: 14px");
|
||
expect(css).toContain("--fb-element-upper-label-font-weight: 600");
|
||
|
||
// 2. Inputs
|
||
await openAccordion(page, "Inputs");
|
||
await setColor(page, "Input color", "eeeeee");
|
||
await setColor(page, "Input border color", "cccccc");
|
||
await setColor(page, "Input Text", "024eff");
|
||
await setDimension(page, "Border Radius", "5");
|
||
await setDimension(page, "Height", "50");
|
||
await setDimension(page, "Font Size", "16");
|
||
await setDimension(page, "Padding X", "20");
|
||
await setDimension(page, "Padding Y", "10");
|
||
await setDimension(page, "Placeholder Opacity", "0.8");
|
||
// Shadow is a text input – use setDimension helper (label→parent→input)
|
||
// because getByLabel relies on for/id linkage which FormLabel doesn't guarantee
|
||
await setDimension(page, "Shadow", "0 4px 6px -1px rgba(0, 0, 0, 0.1)");
|
||
|
||
await page.waitForTimeout(1000);
|
||
css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toContain("--fb-input-background-color: #eeeeee");
|
||
expect(css).toContain("--fb-input-border-color: #cccccc");
|
||
expect(css).toContain("--fb-input-text-color: #024eff");
|
||
expect(css).toContain("--fb-input-placeholder-color:");
|
||
expect(css).toContain("--fb-input-border-radius: 5px");
|
||
expect(css).toContain("--fb-input-height: 50px");
|
||
expect(css).toContain("--fb-input-font-size: 16px");
|
||
expect(css).toContain("--fb-input-padding-x: 20px");
|
||
expect(css).toContain("--fb-input-padding-y: 10px");
|
||
expect(css).toContain("--fb-input-placeholder-opacity: 0.8");
|
||
expect(css).toContain("--fb-input-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1)");
|
||
|
||
// 3. Buttons
|
||
await openAccordion(page, "Buttons");
|
||
await setColor(page, "Button Background", "ff00ff");
|
||
await setColor(page, "Button Text", "ffffff");
|
||
await setDimension(page, "Border Radius", "12");
|
||
await setDimension(page, "Height", "48");
|
||
await setDimension(page, "Font Size", "18");
|
||
await setDimension(page, "Font Weight", "600");
|
||
await setDimension(page, "Padding X", "24");
|
||
await setDimension(page, "Padding Y", "12");
|
||
|
||
await page.waitForTimeout(1000);
|
||
// Partial verification for Buttons to ensure state is good before moving to Options
|
||
css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toContain("--fb-button-bg-color: #ff00ff");
|
||
expect(css).toContain("--fb-button-text-color: #ffffff");
|
||
expect(css).toContain("--fb-button-border-radius: 12px");
|
||
expect(css).toContain("--fb-button-height: 48px");
|
||
expect(css).toContain("--fb-button-font-size: 18px");
|
||
expect(css).toContain("--fb-button-font-weight: 600");
|
||
expect(css).toContain("--fb-button-padding-x: 24px");
|
||
expect(css).toContain("--fb-button-padding-y: 12px");
|
||
|
||
// 4. Options (Radio/Checkbox)
|
||
await openAccordion(page, "Options (Radio/Checkbox)");
|
||
await setColor(page, "Background", "dddddd");
|
||
await setColor(page, "Label Color", "111111");
|
||
await setColor(page, "Border Color", "999999");
|
||
// Note: Border Radius is reused, but we can set it here to be sure
|
||
await setDimension(page, "Border Radius", "6");
|
||
await setDimension(page, "Padding X", "12");
|
||
await setDimension(page, "Padding Y", "8");
|
||
await setDimension(page, "Font Size", "15");
|
||
|
||
await page.waitForTimeout(1000);
|
||
css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toContain("--fb-option-bg-color: #dddddd");
|
||
expect(css).toContain("--fb-option-label-color: #111111");
|
||
expect(css).toContain("--fb-option-border-color: #999999");
|
||
expect(css).toContain("--fb-option-border-radius: 6px");
|
||
expect(css).toContain("--fb-option-padding-x: 12px");
|
||
expect(css).toContain("--fb-option-padding-y: 8px");
|
||
expect(css).toContain("--fb-option-font-size: 15px");
|
||
|
||
// --- Card styling ---
|
||
await openAccordion(page, "Card styling");
|
||
// Toggle progress bar off/on to ensure visibility
|
||
const hideProgressSwitch = page.locator("#hideProgressBar");
|
||
if ((await hideProgressSwitch.getAttribute("aria-checked")) === "true") {
|
||
await hideProgressSwitch.click();
|
||
}
|
||
await setDimension(page, "Track Height", "15");
|
||
await setDimension(page, "Roundness", "20");
|
||
|
||
await page.waitForTimeout(1000);
|
||
css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toContain("--fb-progress-track-height: 15px");
|
||
expect(css).toContain("--fb-progress-track-border-radius: 20px");
|
||
});
|
||
|
||
test("Suggest Colors derives all colors from brand color without changing non-color properties", async ({
|
||
page,
|
||
users,
|
||
}) => {
|
||
const user = await users.create();
|
||
await user.login();
|
||
|
||
// Navigate to Look & Feel settings
|
||
await page.getByRole("link", { name: "Configuration" }).click();
|
||
await page.getByRole("link", { name: "Look & Feel" }).click();
|
||
await page.waitForURL(/\/environments\/[^/]+\/workspace\/look/);
|
||
|
||
// Toggle "Enable custom styling"
|
||
const addCustomStyles = page.getByLabel("Enable custom styling");
|
||
if (!(await addCustomStyles.isChecked())) {
|
||
await addCustomStyles.click();
|
||
}
|
||
|
||
// Set some non-color properties BEFORE suggesting colors, so we can verify they aren't overwritten
|
||
await openAccordion(page, "Survey styling");
|
||
await openAccordion(page, "Inputs");
|
||
await setDimension(page, "Border Radius", "12");
|
||
await setDimension(page, "Padding Y", "20");
|
||
|
||
await openAccordion(page, "Options (Radio/Checkbox)");
|
||
await setDimension(page, "Border Radius", "10");
|
||
await setDimension(page, "Padding Y", "14");
|
||
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Verify non-color CSS vars are set before suggesting
|
||
let cssBefore = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(cssBefore).toContain("--fb-input-border-radius: 12px");
|
||
expect(cssBefore).toContain("--fb-input-padding-y: 20px");
|
||
expect(cssBefore).toContain("--fb-option-border-radius: 10px");
|
||
expect(cssBefore).toContain("--fb-option-padding-y: 14px");
|
||
|
||
// Set a distinctive brand color (rose-600)
|
||
await setColor(page, "Brand color", "e11d48");
|
||
await page.waitForTimeout(500);
|
||
|
||
// Click "Suggest colors" button
|
||
await page.getByRole("button", { name: "Suggest colors" }).click();
|
||
|
||
// Confirm the dialog
|
||
await page.getByRole("button", { name: "Generate" }).click();
|
||
|
||
// Wait for the preview to update with derived colors
|
||
await page.waitForTimeout(1500);
|
||
|
||
const css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toBeDefined();
|
||
|
||
// --- Verify colors ARE derived from brand (not hardcoded) ---
|
||
|
||
// Brand color itself should be present
|
||
expect(css).toContain("--fb-brand-color: #e11d48");
|
||
|
||
// Input background should be brand-tinted, NOT hardcoded #ffffff
|
||
expect(css).toContain("--fb-input-background-color:");
|
||
expect(css).not.toContain("--fb-input-background-color: #ffffff");
|
||
|
||
// Card/survey background should be brand-tinted, NOT hardcoded #ffffff
|
||
expect(css).toContain("--fb-survey-background-color:");
|
||
expect(css).not.toContain("--fb-survey-background-color: #ffffff");
|
||
|
||
// Question/heading color should be derived, NOT the old hardcoded #2b2524
|
||
expect(css).toContain("--fb-heading-color:");
|
||
expect(css).not.toContain("--fb-heading-color: #2b2524");
|
||
|
||
// Input text color should be derived, NOT hardcoded #0f172a
|
||
expect(css).toContain("--fb-input-text-color:");
|
||
expect(css).not.toContain("--fb-input-text-color: #0f172a");
|
||
|
||
// Option label color should be derived, NOT hardcoded #0f172a
|
||
expect(css).toContain("--fb-option-label-color:");
|
||
expect(css).not.toContain("--fb-option-label-color: #0f172a");
|
||
|
||
// Option background should be brand-tinted (same as input bg)
|
||
expect(css).toContain("--fb-option-bg-color:");
|
||
expect(css).not.toContain("--fb-option-bg-color: #ffffff");
|
||
|
||
// --- Verify non-color properties were NOT changed ---
|
||
expect(css).toContain("--fb-input-border-radius: 12px");
|
||
expect(css).toContain("--fb-input-padding-y: 20px");
|
||
expect(css).toContain("--fb-option-border-radius: 10px");
|
||
expect(css).toContain("--fb-option-padding-y: 14px");
|
||
});
|
||
|
||
test("Initial load derives button, progress, input, and option colors from brand color", async ({
|
||
page,
|
||
users,
|
||
}) => {
|
||
const user = await users.create();
|
||
await user.login();
|
||
|
||
// Navigate to Look & Feel settings
|
||
await page.getByRole("link", { name: "Configuration" }).click();
|
||
await page.getByRole("link", { name: "Look & Feel" }).click();
|
||
await page.waitForURL(/\/environments\/[^/]+\/workspace\/look/);
|
||
|
||
// Toggle "Enable custom styling"
|
||
const addCustomStyles = page.getByLabel("Enable custom styling");
|
||
if (!(await addCustomStyles.isChecked())) {
|
||
await addCustomStyles.click();
|
||
}
|
||
|
||
// Wait for the preview to render with default styling
|
||
await page.waitForTimeout(1500);
|
||
|
||
const css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
|
||
expect(css).toBeDefined();
|
||
|
||
// On initial load (no saved styling), button and progress bar should derive from brand color (#64748b)
|
||
// NOT from the old hardcoded dark navy (#0f172a)
|
||
expect(css).not.toContain("--fb-button-bg-color: #0f172a");
|
||
expect(css).not.toContain("--fb-progress-indicator-bg-color: #0f172a");
|
||
|
||
// Input text and option label CSS variables are only emitted when
|
||
// the user explicitly sets them, so on initial load they should NOT
|
||
// be present at all (the CSS-variable fallback from globals.css applies).
|
||
// Verify they are not set to the old hardcoded dark navy value.
|
||
expect(css).not.toContain("--fb-input-text-color: #0f172a");
|
||
expect(css).not.toContain("--fb-option-label-color: #0f172a");
|
||
|
||
// Option background should be brand-tinted, not plain white
|
||
expect(css).toContain("--fb-option-bg-color:");
|
||
|
||
// Headline color should be brand-derived
|
||
expect(css).toContain("--fb-element-headline-color:");
|
||
});
|
||
|
||
test("Survey Specific Styling (Survey Editor Override)", async ({ page, users }) => {
|
||
const user = await users.create();
|
||
await user.login();
|
||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||
|
||
// Create a new survey
|
||
await page.getByText("Start from scratch").click();
|
||
await page.getByRole("button", { name: "Create survey", exact: true }).click();
|
||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/edit$/);
|
||
|
||
// Ensure Welcome Card is active so we can see it
|
||
await page.locator("p", { hasText: "Welcome card" }).first().click({ force: true });
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Check toggle state for Welcome Card
|
||
const welcomeToggle = page.locator("#welcome-toggle");
|
||
if ((await welcomeToggle.getAttribute("data-state")) === "unchecked") {
|
||
await welcomeToggle.click();
|
||
}
|
||
|
||
// Set Headline Text so we can verify it
|
||
// The selector needs to be robust.
|
||
// We try looking for the input labeled "Note" or aria-labelledby="headline"
|
||
const headlineInput = page.locator('[aria-labelledby="headline"]').first();
|
||
await expect(headlineInput).toBeVisible();
|
||
await headlineInput.fill("My Custom Headline");
|
||
await headlineInput.blur(); // Trigger save
|
||
await expect(headlineInput).toHaveText("My Custom Headline");
|
||
|
||
// Navigate to Styling tab
|
||
await page.getByRole("button", { name: "Styling" }).click();
|
||
|
||
// Toggle "Enable custom styling" (Survey override)
|
||
// Note: The label text might be "Add custom styles" in survey editor?
|
||
// Checking previous file: `page.getByLabel("Add custom styles")`
|
||
const addCustomStyles = page
|
||
.locator("label")
|
||
.filter({ hasText: /custom styling|custom styles/i })
|
||
.first();
|
||
const toggleInput = page.locator(`button#${await addCustomStyles.getAttribute("for")}`);
|
||
// If we can't find by ID, try clicking the label or simple check
|
||
if (!(await toggleInput.isChecked())) {
|
||
await addCustomStyles.click();
|
||
}
|
||
|
||
// Apply Overrides
|
||
await openAccordion(page, "Survey styling");
|
||
await openAccordion(page, "Headlines & Descriptions");
|
||
|
||
// Override Headline Color (Blue)
|
||
await setColor(page, "Headline Color", "0000ff");
|
||
await setDimension(page, "Headline Font Size", "30");
|
||
|
||
// Wait for Preview Update
|
||
await page.waitForTimeout(2000);
|
||
|
||
// Click Restart to be sure we are on welcome card
|
||
await page.getByRole("button", { name: "Restart" }).click();
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Check Preview Computed Styles
|
||
const headlinePreview = page.locator("#fbjs .label-headline").first();
|
||
await expect(headlinePreview).toBeVisible();
|
||
await expect(headlinePreview).toHaveText("My Custom Headline");
|
||
|
||
// Verify color override applied (computed style)
|
||
await expect(headlinePreview).toHaveCSS("color", "rgb(0, 0, 255)"); // Blue
|
||
|
||
// Verify font-size override via CSS variable.
|
||
// The computed style can't be checked directly because Tailwind's `text-base`
|
||
// utility is imported with `important` (CSS layer), which outranks the
|
||
// unlayered `!important` from addCustomThemeToDom. The variable IS set
|
||
// correctly though, proving the form → preview pipeline works.
|
||
const editorCss = await page.evaluate(
|
||
() => document.getElementById("formbricks__css__custom")?.innerHTML
|
||
);
|
||
expect(editorCss).toContain("--fb-element-headline-font-size: 30px");
|
||
});
|
||
});
|