feat(ui): gate the Issues feature behind the issues-feature flag (#13090)

This commit is contained in:
Alejandro Bailo authored and GitHub committed 2026-10-19 16:00:26 +02:00
1 parent ba0e78188b
commit 7f9eb2cb11
12 files changed
+318 -26

No files matched your search

@@ -0,0 +1,87 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useFeatureFlagStateMock } = vi.hoisted(() => ({
useFeatureFlagStateMock: vi.fn(),
}));
vi.mock("@/lib/feature-flags", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/feature-flags")>()),
useFeatureFlagState: useFeatureFlagStateMock,
}));
import { FEATURE_FLAG, FEATURE_FLAG_STATE } from "@/lib/feature-flags";
import { FeatureFlagGate } from "./feature-flag-gate";
const renderGate = () =>
render(
<FeatureFlagGate
flag={FEATURE_FLAG.ISSUES}
fallback={<p>off state</p>}
pending={<p>pending state</p>}
>
<p>on state</p>
</FeatureFlagGate>,
);
describe("FeatureFlagGate", () => {
beforeEach(() => {
useFeatureFlagStateMock.mockReset();
});
it("should ask for the given flag", () => {
useFeatureFlagStateMock.mockReturnValue(FEATURE_FLAG_STATE.OFF);
renderGate();
expect(useFeatureFlagStateMock).toHaveBeenCalledWith("issues-feature");
});
it("should render the children when the flag is on", () => {
useFeatureFlagStateMock.mockReturnValue(FEATURE_FLAG_STATE.ON);
renderGate();
expect(screen.getByText("on state")).toBeInTheDocument();
expect(screen.queryByText("off state")).not.toBeInTheDocument();
expect(screen.queryByText("pending state")).not.toBeInTheDocument();
});
it("should render the fallback when the flag is off", () => {
useFeatureFlagStateMock.mockReturnValue(FEATURE_FLAG_STATE.OFF);
renderGate();
expect(screen.getByText("off state")).toBeInTheDocument();
expect(screen.queryByText("on state")).not.toBeInTheDocument();
});
it("should render the pending slot while the flag is unresolved", () => {
useFeatureFlagStateMock.mockReturnValue(FEATURE_FLAG_STATE.PENDING);
renderGate();
expect(screen.getByText("pending state")).toBeInTheDocument();
expect(screen.queryByText("on state")).not.toBeInTheDocument();
expect(screen.queryByText("off state")).not.toBeInTheDocument();
});
it.each([
["off", FEATURE_FLAG_STATE.OFF],
["pending", FEATURE_FLAG_STATE.PENDING],
])(
"should render nothing by default when the flag is %s",
(_label, state) => {
useFeatureFlagStateMock.mockReturnValue(state);
const { container } = render(
<FeatureFlagGate flag={FEATURE_FLAG.ISSUES}>
<p>on state</p>
</FeatureFlagGate>,
);
expect(container).toBeEmptyDOMElement();
},
);
});
@@ -0,0 +1,35 @@
"use client";
import type { ReactNode } from "react";
import {
FEATURE_FLAG_STATE,
type FeatureFlag,
useFeatureFlagState,
} from "@/lib/feature-flags";
interface FeatureFlagGateProps {
flag: FeatureFlag;
children: ReactNode;
// Rendered once the flag resolves to off. Defaults to nothing.
fallback?: ReactNode;
// Rendered while the flag is unresolved. Defaults to nothing so an enabled
// user never sees the off-state fallback flash before PostHog answers, and
// a build that never initializes PostHog shows neither branch.
pending?: ReactNode;
}
// Client gate over a boolean feature flag. Server-rendered children are still
// produced on the server (RSC payload); the gate only decides what the
// browser shows, so keep it for presentation, not for authorization.
export function FeatureFlagGate({
flag,
children,
fallback = null,
pending = null,
}: FeatureFlagGateProps) {
const state = useFeatureFlagState(flag);
if (state === FEATURE_FLAG_STATE.ON) return <>{children}</>;
if (state === FEATURE_FLAG_STATE.PENDING) return <>{pending}</>;
return <>{fallback}</>;
}