Skip to main content
Glama
reflagcom

Bucket Feature Flags MCP Server

Official
by reflagcom
feedback-widget.browser.spec.ts13.3 kB
import { randomUUID } from "crypto"; import { expect, Locator, Page, test } from "@playwright/test"; import { InitOptions } from "../../src/client"; import { DEFAULT_TRANSLATIONS } from "../../src/feedback/ui/config/defaultTranslations"; import { FeedbackTranslations } from "../../src/feedback/ui/types"; import { feedbackContainerId, propagatedEvents } from "../../src/ui/constants"; const KEY = randomUUID(); const API_HOST = `https://front.reflag.com`; const WINDOW_WIDTH = 1280; const WINDOW_HEIGHT = 720; declare global { interface Window { eventsFired: Record<string, boolean>; } } function pick<T>(options: T[]): T { return options[Math.floor(Math.random() * options.length)]; } async function getOpenedWidgetContainer( page: Page, initOptions: Omit<InitOptions, "publishableKey"> = {}, ) { await page.goto("http://localhost:8001/test/e2e/empty.html"); // Mock API calls await page.route(`${API_HOST}/user`, async (route) => { await route.fulfill({ status: 200 }); }); await page.route(`${API_HOST}/features/evaluated*`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ success: true, features: {}, }), }); }); // Golden path requests await page.evaluate(` ;(async () => { const { ReflagClient } = await import("/dist/reflag-browser-sdk.mjs"); const reflag = new ReflagClient({publishableKey: "${KEY}", user: {id: "foo"}, company: {id: "bar"}, ...${JSON.stringify(initOptions ?? {})}}); await reflag.initialize(); await reflag.requestFeedback({ flagKey: "flag1", title: "baz", }); })() `); return page.locator(`#${feedbackContainerId}`); } async function getGiveFeedbackPageContainer( page: Page, initOptions: Omit<InitOptions, "publishableKey"> = {}, ) { await page.goto("http://localhost:8001/test/e2e/give-feedback-button.html"); // Mock API calls await page.route(`${API_HOST}/user`, async (route) => { await route.fulfill({ status: 200 }); }); await page.route(`${API_HOST}/features/evaluated*`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ success: true, features: {}, }), }); }); // Golden path requests await page.evaluate(` ;(async () => { const { ReflagClient } = await import("/dist/reflag-browser-sdk.mjs"); const reflag = new ReflagClient({publishableKey: "${KEY}", user: {id: "foo"}, company: {id: "bar"}, ...${JSON.stringify(initOptions ?? {})}}); await reflag.initialize(); console.log("setup clicky", document.querySelector("#give-feedback-button")) document.querySelector("#give-feedback-button")?.addEventListener("click", () => { console.log("cliked!"); reflag.requestFeedback({ flagKey: "flag1", title: "baz", }); }); })() `); return page.locator(`#${feedbackContainerId}`); } async function setScore(container: Locator, score: number) { await new Promise((resolve) => setTimeout(resolve, 50)); // allow react to update its state await container .locator(`#reflag-feedback-score-${score}`) .dispatchEvent("click"); } async function setComment(container: Locator, comment: string) { await container.locator("#reflag-feedback-comment-label").fill(comment); } async function submitForm(container: Locator) { await container.locator(".form-expanded-content").getByRole("button").click(); } test.beforeEach(async ({ page, browserName }) => { // Log any calls to front.reflag.com which aren't mocked by subsequent // `page.route` calls. With page.route, the last matching mock takes // precedence, so this logs any which may have been missed, and responds // with a 200 to prevent an internet request. await page.route(/^https:\/\/front\.reflag\.com.*/, async (route) => { const meta = `${route.request().method()} ${route.request().url()}`; console.debug(`\n Unmocked request: [${browserName}] > ${meta}`); console.debug(`Sent stub mock response: [${browserName}] < ${meta} 200\n`); await route.fulfill({ status: 200, body: "{}" }); }); // Mock prompting-init as if prompting is `disabled` for all tests. await page.route(`${API_HOST}/feedback/prompting-init`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ success: false }), }); }); }); test("Opens a feedback widget", async ({ page }) => { const container = await getOpenedWidgetContainer(page); await expect(container).toBeAttached(); await expect(container.locator("dialog")).toHaveAttribute("open", ""); }); test("Opens a feedback widget multiple times in same session", async ({ page, }) => { const container = await getGiveFeedbackPageContainer(page); await page.getByTestId("give-feedback-button").click(); await expect(container).toBeAttached(); await expect(container.locator("dialog")).toHaveAttribute("open", ""); await container.locator("dialog .close").click(); await expect(container.locator("dialog")).not.toHaveAttribute("open", ""); await page.getByTestId("give-feedback-button").click(); await expect(container).toBeAttached(); await expect(container.locator("dialog")).toHaveAttribute("open", ""); }); test("Opens a feedback widget in the bottom right by default", async ({ page, }) => { const container = await getOpenedWidgetContainer(page); await expect(container).toBeAttached(); const bbox = await container.locator("dialog").boundingBox(); expect(bbox?.x).toEqual(WINDOW_WIDTH - bbox!.width - 16); expect(bbox?.y).toBeGreaterThan(WINDOW_HEIGHT - bbox!.height - 30); // Account for browser differences expect(bbox?.y).toBeLessThan(WINDOW_HEIGHT - bbox!.height); }); test("Opens a feedback widget in the correct position when overridden", async ({ page, }) => { const container = await getOpenedWidgetContainer(page, { feedback: { ui: { position: { type: "DIALOG", placement: "top-left", }, }, }, }); await expect(container).toBeAttached(); const bbox = await container.locator("dialog").boundingBox(); expect(bbox?.x).toEqual(16); expect(bbox?.y).toBeGreaterThan(0); // Account for browser differences expect(bbox?.y).toBeLessThanOrEqual(16); }); test("Opens a feedback widget with the correct translations", async ({ page, }) => { const translations: Partial<FeedbackTranslations> = { ScoreStatusDescription: "Choisissez une note et laissez un commentaire", ScoreVeryDissatisfiedLabel: "Très insatisfait", ScoreDissatisfiedLabel: "Insatisfait", ScoreNeutralLabel: "Neutre", ScoreSatisfiedLabel: "Satisfait", ScoreVerySatisfiedLabel: "Très satisfait", SendButton: "Envoyer", }; const container = await getOpenedWidgetContainer(page, { feedback: { ui: { translations, }, }, }); await expect(container).toBeAttached(); await expect(container).toContainText(translations.ScoreStatusDescription!); await expect(container).toContainText( translations.ScoreVeryDissatisfiedLabel!, ); await expect(container).toContainText(translations.ScoreDissatisfiedLabel!); await expect(container).toContainText(translations.ScoreNeutralLabel!); await expect(container).toContainText(translations.ScoreSatisfiedLabel!); await expect(container).toContainText(translations.ScoreVerySatisfiedLabel!); await expect(container).toContainText(translations.SendButton!); }); test("Sends a request when choosing a score immediately", async ({ page }) => { const expectedScore = pick([1, 2, 3, 4, 5]); let sentJSON: object | null = null; await page.route(`${API_HOST}/feedback`, async (route) => { sentJSON = route.request().postDataJSON(); await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await setScore(container, expectedScore); await expect .poll(() => sentJSON) .toEqual({ companyId: "bar", key: "flag1", score: expectedScore, question: "baz", userId: "foo", source: "widget", }); }); test("Shows a success message after submitting a score", async ({ page }) => { await page.route(`${API_HOST}/feedback`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await expect( container.getByText(DEFAULT_TRANSLATIONS.ScoreStatusDescription), ).toHaveCSS("opacity", "1"); await expect( container.getByText(DEFAULT_TRANSLATIONS.ScoreStatusReceived), ).toHaveCSS("opacity", "0"); await setScore(container, 3); await expect( container.getByText(DEFAULT_TRANSLATIONS.ScoreStatusDescription), ).toHaveCSS("opacity", "0"); await expect( container.getByText(DEFAULT_TRANSLATIONS.ScoreStatusReceived), ).toHaveCSS("opacity", "1"); }); test("Updates the score on every change", async ({ page }) => { let lastSentJSON: object | null = null; await page.route(`${API_HOST}/feedback`, async (route) => { lastSentJSON = route.request().postDataJSON(); await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await setScore(container, 1); await setScore(container, 5); await setScore(container, 3); await expect .poll(() => lastSentJSON) .toEqual({ feedbackId: "123", companyId: "bar", key: "flag1", question: "baz", score: 3, userId: "foo", source: "widget", }); }); test("Shows the comment field after submitting a score", async ({ page }) => { await page.route(`${API_HOST}/feedback`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await expect(container.locator(".form-expanded-content")).toHaveCSS( "opacity", "0", ); await setScore(container, 1); await expect(container.locator(".form-expanded-content")).toHaveCSS( "opacity", "1", ); }); test("Sends a request with both the score and comment when submitting", async ({ page, }) => { const expectedComment = `This is my comment: ${Math.random()}`; const expectedScore = pick([1, 2, 3, 4, 5]); let sentJSON: object | null = null; await page.route(`${API_HOST}/feedback`, async (route) => { sentJSON = route.request().postDataJSON(); await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await setScore(container, expectedScore); await setComment(container, expectedComment); await submitForm(container); expect(sentJSON).toEqual({ comment: expectedComment, score: expectedScore, companyId: "bar", question: "baz", key: "flag1", feedbackId: "123", userId: "foo", source: "widget", }); }); test("Shows a success message after submitting", async ({ page }) => { await page.route(`${API_HOST}/feedback`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await setScore(container, 3); await setComment(container, "Test comment!"); await submitForm(container); await expect( container.getByText(DEFAULT_TRANSLATIONS.SuccessMessage), ).toBeVisible(); }); test("Closes the dialog shortly after submitting", async ({ page }) => { await page.route(`${API_HOST}/feedback`, async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ feedbackId: "123" }), contentType: "application/json", }); }); const container = await getOpenedWidgetContainer(page); await setScore(container, 3); await setComment(container, "Test comment!"); await submitForm(container); await expect(container.locator("dialog")).not.toHaveAttribute("open", ""); }); test("Blocks event propagation to the containing document", async ({ page, }) => { const container = await getOpenedWidgetContainer(page); const textarea = container.locator('textarea[name="comment"]'); await page.evaluate( ({ trackedEvents }) => { window.eventsFired = {}; for (const event of trackedEvents) { document.addEventListener(event, () => { window.eventsFired[event] = true; }); } }, { trackedEvents: propagatedEvents }, ); await textarea.focus(); // Fires 'keydown', 'keyup' and 'keypress' events await page.keyboard.type("Hello World"); const firedEvents = await page.evaluate(() => { return window.eventsFired; }); // No events are allowed to fire, object should be empty expect(firedEvents).toEqual({}); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reflagcom/bucket-javascript-sdk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server