mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(desktop): welcome banner overlap and missing dismiss control (#5330)
## Problem The `WelcomeComposerGuidanceLayer` in the `#Welcome` channel was positioned with `absolute inset-x-0 bottom-full z-[-1]` — outside the `composerWrapperRef` measurement boundary. `useComposerHeightPadding` observes `composerWrapperRef`'s block size to set `paddingBottom` on the timeline scroll container, but the absolutely-positioned layer didn't contribute to that size. The banner sat directly on top of the newest message, blocking the thread affordance on that message, and had no manual dismiss control. ## Fix **Overlap**: Changed `WelcomeComposerGuidanceLayer` from `absolute inset-x-0 bottom-full z-[-1]` to `relative` (in normal flow). As a normal-flow child of `composer-dock`, the layer's full height is now measured by the ResizeObserver and fed into the timeline's `paddingBottom`, so the newest message is always fully visible and its thread affordance is always clickable while the banner shows. **Dismiss**: Added an `X` close button (`data-testid="welcome-composer-dismiss-button"`) on the prompt state. Clicking fires `onDismiss`, which drives `dismissing → hidden` immediately (same slide-down animation as the auto-dismiss path) and marks the channel ID as completed in the session ref so the banner does not reappear on channel re-entry within the session. **Refactor**: Extracted the banner state machine (refs, timers, `useEffect`s, and callbacks) from `ChannelPane.tsx` into `useWelcomeComposerBanner.ts`. This keeps `ChannelPane.tsx` well under the 1000-line file-size ratchet and makes the state machine independently testable. ## Changed files - `desktop/src/features/channels/ui/WelcomeComposerBanner.tsx` — `WelcomeComposerGuidanceLayer` positioning fix; `onDismiss` prop; dismiss button; `overflow-hidden` / `mb-0` / `flex-1` cleanup - `desktop/src/features/channels/ui/ChannelPane.tsx` — remove inline banner state machine, use `useWelcomeComposerBanner` hook, pass `onDismiss` - `desktop/src/features/channels/ui/useWelcomeComposerBanner.ts` — new hook owning all banner state --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -43,12 +43,8 @@ import { ChannelComposerActivityAccessory } from "@/features/channels/ui/Channel
|
||||
import {
|
||||
containsWelcomePersonaMention,
|
||||
WelcomeComposerGuidanceLayer,
|
||||
WELCOME_COMPOSER_BANNER_DISMISS_DURATION_SECONDS,
|
||||
WELCOME_COMPOSER_BANNER_HIDE_BUFFER_MS,
|
||||
WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS,
|
||||
WELCOME_PERSONA_ROTATION_MS,
|
||||
type WelcomeComposerBannerState,
|
||||
} from "@/features/channels/ui/WelcomeComposerBanner";
|
||||
import { useWelcomeComposerBanner } from "@/features/channels/ui/useWelcomeComposerBanner";
|
||||
import { mentionsKnownAgent } from "@/features/channels/ui/ChannelPane.helpers";
|
||||
import { HuddleStartingView, HuddleTranscriptIntro } from "@/features/huddle";
|
||||
import { useChannelIntro } from "@/features/channels/ui/useChannelIntro";
|
||||
@@ -168,11 +164,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
const timelineScrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const messageTimelineRef = React.useRef<MessageTimelineHandle>(null);
|
||||
const composerWrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const completedWelcomeBannerChannelIdsRef = React.useRef(new Set<string>());
|
||||
const welcomeComposerDismissTimerRef = React.useRef<number | null>(null);
|
||||
const welcomeComposerHideTimerRef = React.useRef<number | null>(null);
|
||||
const [welcomeComposerBannerState, setWelcomeComposerBannerState] =
|
||||
React.useState<WelcomeComposerBannerState>("prompt");
|
||||
const { goChannel } = useAppNavigation();
|
||||
const prepareDmSendChannel = usePrepareDmSendChannel(
|
||||
activeChannel,
|
||||
@@ -221,36 +212,11 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
"css-variable",
|
||||
() => messageTimelineRef.current?.settleAtBottom() ?? false,
|
||||
);
|
||||
const clearWelcomeComposerDismissTimer = React.useCallback(() => {
|
||||
if (welcomeComposerDismissTimerRef.current !== null) {
|
||||
window.clearTimeout(welcomeComposerDismissTimerRef.current);
|
||||
welcomeComposerDismissTimerRef.current = null;
|
||||
}
|
||||
if (welcomeComposerHideTimerRef.current !== null) {
|
||||
window.clearTimeout(welcomeComposerHideTimerRef.current);
|
||||
welcomeComposerHideTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
React.useEffect(
|
||||
() => () => clearWelcomeComposerDismissTimer(),
|
||||
[clearWelcomeComposerDismissTimer],
|
||||
);
|
||||
React.useEffect(() => {
|
||||
clearWelcomeComposerDismissTimer();
|
||||
if (
|
||||
activeChannelId &&
|
||||
isActiveWelcomeChannel &&
|
||||
completedWelcomeBannerChannelIdsRef.current.has(activeChannelId)
|
||||
) {
|
||||
setWelcomeComposerBannerState("hidden");
|
||||
return;
|
||||
}
|
||||
setWelcomeComposerBannerState("prompt");
|
||||
}, [
|
||||
activeChannelId,
|
||||
clearWelcomeComposerDismissTimer,
|
||||
isActiveWelcomeChannel,
|
||||
]);
|
||||
const {
|
||||
bannerState: welcomeComposerBannerState,
|
||||
completeBanner: completeWelcomeComposerBanner,
|
||||
dismissBanner: handleDismissWelcomeBanner,
|
||||
} = useWelcomeComposerBanner(activeChannelId, isActiveWelcomeChannel);
|
||||
const isEditInThread =
|
||||
editTarget != null &&
|
||||
threadHeadMessage != null &&
|
||||
@@ -330,31 +296,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
|
||||
return pubkeys;
|
||||
}, [activityAgents, agentPubkeys, agentSessionAgents]);
|
||||
const completeWelcomeComposerBanner = React.useCallback(() => {
|
||||
if (!activeChannelId || !isActiveWelcomeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearWelcomeComposerDismissTimer();
|
||||
completedWelcomeBannerChannelIdsRef.current.add(activeChannelId);
|
||||
setWelcomeComposerBannerState("complete");
|
||||
welcomeComposerDismissTimerRef.current = window.setTimeout(() => {
|
||||
setWelcomeComposerBannerState("dismissing");
|
||||
welcomeComposerDismissTimerRef.current = null;
|
||||
welcomeComposerHideTimerRef.current = window.setTimeout(
|
||||
() => {
|
||||
setWelcomeComposerBannerState("hidden");
|
||||
welcomeComposerHideTimerRef.current = null;
|
||||
},
|
||||
WELCOME_COMPOSER_BANNER_DISMISS_DURATION_SECONDS * 1000 +
|
||||
WELCOME_COMPOSER_BANNER_HIDE_BUFFER_MS,
|
||||
);
|
||||
}, WELCOME_PERSONA_ROTATION_MS + WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS);
|
||||
}, [
|
||||
activeChannelId,
|
||||
clearWelcomeComposerDismissTimer,
|
||||
isActiveWelcomeChannel,
|
||||
]);
|
||||
const handleSendMessage = React.useCallback(
|
||||
async (
|
||||
content: string,
|
||||
@@ -739,6 +680,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
>
|
||||
{isActiveWelcomeChannel && !timeoutState.active ? (
|
||||
<WelcomeComposerGuidanceLayer
|
||||
onDismiss={handleDismissWelcomeBanner}
|
||||
settingUp={welcomeKickoffSettingUp}
|
||||
state={welcomeComposerBannerState}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { Bot, Check } from "lucide-react";
|
||||
import { Bot, Check, X } from "lucide-react";
|
||||
|
||||
import { ComposerDockGlassBackdrop } from "@/features/messages/ui/ComposerDockBackdrop";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -291,9 +291,15 @@ type WelcomeComposerBannerProps = {
|
||||
* banner during this window.
|
||||
*/
|
||||
settingUp?: boolean;
|
||||
/**
|
||||
* Called when the user dismisses the banner manually via the close button.
|
||||
* Only rendered while `state === "prompt"`.
|
||||
*/
|
||||
onDismiss?: () => void;
|
||||
};
|
||||
|
||||
export function WelcomeComposerBanner({
|
||||
onDismiss,
|
||||
settingUp = false,
|
||||
state,
|
||||
}: WelcomeComposerBannerProps) {
|
||||
@@ -307,7 +313,7 @@ export function WelcomeComposerBanner({
|
||||
animate={{
|
||||
height: state === "dismissing" ? 0 : "auto",
|
||||
}}
|
||||
className="overflow-visible"
|
||||
className="overflow-hidden"
|
||||
initial={false}
|
||||
transition={{
|
||||
duration:
|
||||
@@ -326,7 +332,7 @@ export function WelcomeComposerBanner({
|
||||
: 0,
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-[1] mx-5 -mb-3 flex items-center gap-2 rounded-t-2xl border border-b-0 px-4 pb-5 pt-2.5 text-sm leading-5 transition-colors",
|
||||
"relative z-[1] mx-5 mb-0 flex items-center gap-2 rounded-t-2xl border border-b-0 px-4 pb-5 pt-2.5 text-sm leading-5 transition-colors",
|
||||
state !== "prompt"
|
||||
? "border-emerald-500/30 bg-emerald-500/15 text-foreground"
|
||||
: "border-border/60 bg-muted/55 text-muted-foreground",
|
||||
@@ -376,7 +382,7 @@ export function WelcomeComposerBanner({
|
||||
{state !== "prompt" ? (
|
||||
<motion.span
|
||||
animate="animate"
|
||||
className="min-w-0"
|
||||
className="min-w-0 flex-1"
|
||||
data-animation-target="success-copy"
|
||||
initial="initial"
|
||||
key="complete-copy"
|
||||
@@ -387,7 +393,7 @@ export function WelcomeComposerBanner({
|
||||
) : settingUp ? (
|
||||
<motion.span
|
||||
animate="animate"
|
||||
className="min-w-0"
|
||||
className="min-w-0 flex-1"
|
||||
data-testid="welcome-composer-setting-up-copy"
|
||||
exit="exit"
|
||||
initial="initial"
|
||||
@@ -399,7 +405,7 @@ export function WelcomeComposerBanner({
|
||||
) : (
|
||||
<motion.span
|
||||
animate="animate"
|
||||
className="min-w-0"
|
||||
className="min-w-0 flex-1"
|
||||
exit="exit"
|
||||
initial="initial"
|
||||
key="prompt-copy"
|
||||
@@ -410,6 +416,17 @@ export function WelcomeComposerBanner({
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{state === "prompt" && onDismiss && !settingUp ? (
|
||||
<button
|
||||
aria-label="Dismiss hint"
|
||||
className="ml-auto flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground/60 transition-colors hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-testid="welcome-composer-dismiss-button"
|
||||
onClick={onDismiss}
|
||||
type="button"
|
||||
>
|
||||
<X aria-hidden className="h-3 w-3" />
|
||||
</button>
|
||||
) : null}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
@@ -422,22 +439,22 @@ type WelcomeComposerGuidanceLayerProps = WelcomeComposerBannerProps & {
|
||||
|
||||
export function WelcomeComposerGuidanceLayer({
|
||||
children,
|
||||
onDismiss,
|
||||
settingUp,
|
||||
state,
|
||||
}: WelcomeComposerGuidanceLayerProps) {
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-full z-[-1]"
|
||||
data-testid="welcome-composer-guidance-layer"
|
||||
>
|
||||
<div className="relative">
|
||||
<ComposerDockGlassBackdrop
|
||||
className="absolute inset-x-5 top-0 bottom-3 z-0 rounded-t-2xl"
|
||||
testId="welcome-composer-guidance-backdrop"
|
||||
/>
|
||||
{children}
|
||||
<WelcomeComposerBanner settingUp={settingUp} state={state} />
|
||||
</div>
|
||||
<div className="relative" data-testid="welcome-composer-guidance-layer">
|
||||
<ComposerDockGlassBackdrop
|
||||
className="absolute inset-x-5 top-0 bottom-3 z-0 rounded-t-2xl"
|
||||
testId="welcome-composer-guidance-backdrop"
|
||||
/>
|
||||
{children}
|
||||
<WelcomeComposerBanner
|
||||
onDismiss={onDismiss}
|
||||
settingUp={settingUp}
|
||||
state={state}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
99
desktop/src/features/channels/ui/useWelcomeComposerBanner.ts
Normal file
99
desktop/src/features/channels/ui/useWelcomeComposerBanner.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
WELCOME_COMPOSER_BANNER_DISMISS_DURATION_SECONDS,
|
||||
WELCOME_COMPOSER_BANNER_HIDE_BUFFER_MS,
|
||||
WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS,
|
||||
WELCOME_PERSONA_ROTATION_MS,
|
||||
type WelcomeComposerBannerState,
|
||||
} from "@/features/channels/ui/WelcomeComposerBanner";
|
||||
|
||||
/**
|
||||
* Manages the Welcome-channel composer hint banner's state machine.
|
||||
*
|
||||
* Tracks which channels have been completed within the session so the banner
|
||||
* stays hidden on re-entry. Exposes three transitions:
|
||||
* - `completeBanner`: agent-mention path — plays the "Nice work." success
|
||||
* animation before auto-dismissing.
|
||||
* - `dismissBanner`: manual X-button path — immediately begins the slide-down
|
||||
* dismiss animation.
|
||||
*/
|
||||
export function useWelcomeComposerBanner(
|
||||
activeChannelId: string | null,
|
||||
isActiveWelcomeChannel: boolean,
|
||||
): {
|
||||
bannerState: WelcomeComposerBannerState;
|
||||
completeBanner: () => void;
|
||||
dismissBanner: () => void;
|
||||
} {
|
||||
const completedChannelIdsRef = React.useRef(new Set<string>());
|
||||
const dismissTimerRef = React.useRef<number | null>(null);
|
||||
const hideTimerRef = React.useRef<number | null>(null);
|
||||
const [bannerState, setBannerState] =
|
||||
React.useState<WelcomeComposerBannerState>("prompt");
|
||||
|
||||
const clearTimers = React.useCallback(() => {
|
||||
if (dismissTimerRef.current !== null) {
|
||||
window.clearTimeout(dismissTimerRef.current);
|
||||
dismissTimerRef.current = null;
|
||||
}
|
||||
if (hideTimerRef.current !== null) {
|
||||
window.clearTimeout(hideTimerRef.current);
|
||||
hideTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => () => clearTimers(), [clearTimers]);
|
||||
|
||||
React.useEffect(() => {
|
||||
clearTimers();
|
||||
if (
|
||||
activeChannelId &&
|
||||
isActiveWelcomeChannel &&
|
||||
completedChannelIdsRef.current.has(activeChannelId)
|
||||
) {
|
||||
setBannerState("hidden");
|
||||
return;
|
||||
}
|
||||
setBannerState("prompt");
|
||||
}, [activeChannelId, clearTimers, isActiveWelcomeChannel]);
|
||||
|
||||
const scheduleHide = React.useCallback(() => {
|
||||
hideTimerRef.current = window.setTimeout(
|
||||
() => {
|
||||
setBannerState("hidden");
|
||||
hideTimerRef.current = null;
|
||||
},
|
||||
WELCOME_COMPOSER_BANNER_DISMISS_DURATION_SECONDS * 1000 +
|
||||
WELCOME_COMPOSER_BANNER_HIDE_BUFFER_MS,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const completeBanner = React.useCallback(() => {
|
||||
if (!activeChannelId || !isActiveWelcomeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimers();
|
||||
completedChannelIdsRef.current.add(activeChannelId);
|
||||
setBannerState("complete");
|
||||
dismissTimerRef.current = window.setTimeout(() => {
|
||||
setBannerState("dismissing");
|
||||
dismissTimerRef.current = null;
|
||||
scheduleHide();
|
||||
}, WELCOME_PERSONA_ROTATION_MS + WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS);
|
||||
}, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]);
|
||||
|
||||
const dismissBanner = React.useCallback(() => {
|
||||
if (!activeChannelId || !isActiveWelcomeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimers();
|
||||
completedChannelIdsRef.current.add(activeChannelId);
|
||||
setBannerState("dismissing");
|
||||
scheduleHide();
|
||||
}, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]);
|
||||
|
||||
return { bannerState, completeBanner, dismissBanner };
|
||||
}
|
||||
@@ -207,6 +207,7 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
|
||||
.locator("div")
|
||||
.boundingBox();
|
||||
const guidanceLayer = page.getByTestId("welcome-composer-guidance-layer");
|
||||
const guidanceLayerBox = await guidanceLayer.boundingBox();
|
||||
const guidanceBackdrop = page.getByTestId(
|
||||
"welcome-composer-guidance-backdrop",
|
||||
);
|
||||
@@ -217,6 +218,7 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
|
||||
!personaMentionBox ||
|
||||
!composerBox ||
|
||||
!dockBackdropBox ||
|
||||
!guidanceLayerBox ||
|
||||
!guidanceBackdropBox
|
||||
) {
|
||||
throw new Error("Could not measure welcome composer banner layout");
|
||||
@@ -226,23 +228,20 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
|
||||
await composer.getByTestId("welcome-composer-guide-banner").count(),
|
||||
).toBe(0);
|
||||
expect(bannerBox.y).toBeLessThan(composerBox.y);
|
||||
expect(bannerBox.y + bannerBox.height).toBeGreaterThan(composerBox.y);
|
||||
expect(Math.abs(dockBackdropBox.y - composerBox.y)).toBeLessThanOrEqual(1);
|
||||
// Banner is in normal flow above the composer, no overlap.
|
||||
expect(bannerBox.y + bannerBox.height).toBeLessThanOrEqual(composerBox.y);
|
||||
// The dock backdrop is absolute inset-y-0 inside composer-dock, which now
|
||||
// contains the guidance layer + composer in flow, so its top aligns with the
|
||||
// guidance layer top (not the composer top).
|
||||
expect(Math.abs(dockBackdropBox.y - guidanceLayerBox.y)).toBeLessThanOrEqual(
|
||||
1,
|
||||
);
|
||||
expect(guidanceBackdropBox.y).toBeLessThanOrEqual(bannerBox.y);
|
||||
expect(
|
||||
Math.abs(
|
||||
guidanceBackdropBox.y + guidanceBackdropBox.height - composerBox.y,
|
||||
),
|
||||
).toBeLessThanOrEqual(1);
|
||||
const [guidanceZIndex, backdropZIndex] = await Promise.all([
|
||||
guidanceLayer.evaluate((element) =>
|
||||
Number(window.getComputedStyle(element).zIndex),
|
||||
),
|
||||
page
|
||||
.getByTestId("composer-dock-backdrop")
|
||||
.evaluate((element) => Number(window.getComputedStyle(element).zIndex)),
|
||||
]);
|
||||
expect(guidanceZIndex).toBeLessThan(backdropZIndex);
|
||||
// The guidance backdrop extends bottom-3 (12px) short of the banner's bottom,
|
||||
// visually connecting up to the composer.
|
||||
expect(guidanceBackdropBox.y + guidanceBackdropBox.height).toBeLessThan(
|
||||
composerBox.y,
|
||||
);
|
||||
expect(
|
||||
await page
|
||||
.getByTestId("channel-composer-overlay")
|
||||
@@ -3170,6 +3169,59 @@ test("finishing onboarding creates starter channels and focuses welcome-everyone
|
||||
await expectWelcomeComposerBannerCompletesAfterPersonaMention(page);
|
||||
});
|
||||
|
||||
test("welcome-everywhere banner: X dismiss removes the guidance surface", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
||||
await completeProfileOnboarding(page);
|
||||
|
||||
const banner = page.getByTestId("welcome-composer-guide-banner");
|
||||
const guidanceLayer = page.getByTestId("welcome-composer-guidance-layer");
|
||||
const dismissButton = page.getByTestId("welcome-composer-dismiss-button");
|
||||
|
||||
// Banner and guidance layer are visible in the prompt state.
|
||||
await expect(banner).toBeVisible();
|
||||
await expect(guidanceLayer).toBeVisible();
|
||||
await expect(dismissButton).toBeVisible();
|
||||
|
||||
await dismissButton.click();
|
||||
|
||||
// After dismiss the entire guidance surface must be gone.
|
||||
await expect(banner).toHaveCount(0, { timeout: 2_000 });
|
||||
await expect(guidanceLayer).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("welcome-everywhere banner: dismiss persists after channel re-entry", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
|
||||
await installMockBridge(page, undefined, { skipOnboardingSeed: true });
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("onboarding-display-name").fill("Morty QA");
|
||||
await completeProfileOnboarding(page);
|
||||
|
||||
const banner = page.getByTestId("welcome-composer-guide-banner");
|
||||
|
||||
await expect(banner).toBeVisible();
|
||||
await page.getByTestId("welcome-composer-dismiss-button").click();
|
||||
await expect(banner).toHaveCount(0, { timeout: 2_000 });
|
||||
|
||||
// Leave the Welcome channel.
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toContainText("general");
|
||||
await expect(banner).toHaveCount(0);
|
||||
|
||||
// Return — banner must stay hidden.
|
||||
await page.getByTestId("channel-welcome-everyone").click();
|
||||
await expect(page.getByTestId("chat-title")).toContainText("Welcome");
|
||||
await expect(banner).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("initial profile read failures still hold incomplete users in onboarding", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user