diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index 482af79520..c3ab69c786 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -17,7 +17,10 @@ import { useReloadShortcut } from "@/app/useReloadShortcut"; import { KnownAgentPubkeysProvider } from "@/features/agents/useKnownAgentPubkeys"; import { useAppOnboardingState } from "@/features/onboarding/hooks"; import { useMachineOnboardingState } from "@/features/onboarding/machineOnboarding"; -import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding"; +import { + type FirstCommunityPage, + useCommunityOnboarding, +} from "@/features/onboarding/communityOnboarding"; import { CommunityOnboardingFlow } from "@/features/onboarding/ui/CommunityOnboardingFlow"; import { MachineOnboardingFlow, @@ -275,8 +278,8 @@ function CommunityApp({ const communityOnboarding = useCommunityOnboarding(); const connectingTransactionRef = useRef(null); const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false); - const [resumeFirstCommunityJoin, setResumeFirstCommunityJoin] = - useState(false); + const [resumeFirstCommunityPage, setResumeFirstCommunityPage] = + useState(null); // Surface nest-related backend events (repos-dir errors, legacy migration) // as toasts. Mounted before useCommunityInit so the listeners are registered @@ -354,7 +357,7 @@ function CommunityApp({ } if (communities.length === 1) { if (transaction.source === "first-community") { - setResumeFirstCommunityJoin(true); + setResumeFirstCommunityPage(transaction.firstCommunityPage ?? "join"); } clearCommunities(); return; @@ -410,7 +413,7 @@ function CommunityApp({ // Show welcome setup for first-run users with no communities appContent = ( ); diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index 46c1286d3f..cc4c263859 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -1,5 +1,6 @@ import * as React from "react"; -import { AlertCircle, CheckCircle2, LoaderCircle } from "lucide-react"; +import { AlertCircle, LoaderCircle } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { bindBuilderlabIdentity, @@ -24,17 +25,47 @@ import { useCommunityOnboarding } from "@/features/onboarding/communityOnboardin import { useIdentityQuery } from "@/shared/api/hooks"; import { safeNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; +import { Card } from "@/shared/ui/card"; import { Input } from "@/shared/ui/input"; import { OnboardingFooter } from "@/features/onboarding/ui/OnboardingFooter"; +import { + ONBOARDING_INK_ICON_CLASS, + ONBOARDING_PRIMARY_CTA_CLASS, +} from "@/features/onboarding/ui/OnboardingChrome"; +import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/shared/ui/dialog"; + +const FUZZY_SURFACE_CLASS = + "relative left-1/2 w-[min(calc(100%+12rem),calc(100vw-2rem))] max-w-[1040px] -translate-x-1/2 px-20 pb-14 pt-20 !text-[rgb(var(--buzz-hosted-community-surface-fg))] [--buzz-card-textured-min-height:224px]"; +const COMMUNITY_LIST_CLASS = "mx-auto w-full max-w-[520px] text-left"; +const COMMUNITY_ROW_CLASS = + "flex min-h-[5.75rem] items-center justify-between gap-8 py-4 text-sm"; +const COMMUNITY_DIVIDER_CLASS = + "border-b-[0.5px] border-[rgb(var(--buzz-hosted-community-divider-border)/0.5)]"; +const COMMUNITY_ACTION_CLASS = + "h-[2.375rem] min-w-32 shrink-0 rounded-full bg-[rgb(var(--buzz-hosted-community-action-bg))] px-6 text-sm text-foreground shadow-none hover:bg-[rgb(var(--buzz-hosted-community-action-bg-hover))]"; +const PAGE_CTA_CLASS = `${ONBOARDING_PRIMARY_CTA_CLASS} w-36 shadow-none`; +const PAGE_BACK_CLASS = + "h-[2.375rem] w-36 rounded-full bg-foreground/10 px-6 shadow-none hover:bg-foreground/15"; +const MODAL_PRIMARY_ACTION_CLASS = `${ONBOARDING_PRIMARY_CTA_CLASS} !text-[rgb(var(--buzz-hosted-community-modal-action-fg))]`; +const MODAL_BACK_ACTION_CLASS = + "h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"; export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const onboarding = useCommunityOnboarding(); + const shouldReduceMotion = useReducedMotion(); const localPubkey = useIdentityQuery().data?.pubkey ?? null; const [auth, setAuth] = React.useState(null); const [identity, setIdentity] = React.useState( null, ); const [communities, setCommunities] = React.useState([]); + const [showCreate, setShowCreate] = React.useState(false); const [name, setName] = React.useState(""); const [availability, setAvailability] = React.useState(null); const [checkingName, setCheckingName] = React.useState(false); @@ -100,12 +131,14 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { }); }; - const cancelSignIn = () => { + const cancelSignInAndGoBack = () => { loginAttempt.current += 1; setAction(null); setError(null); - void cancelBuilderlabLogin().catch((cause) => { - setError(cause instanceof Error ? cause.message : String(cause)); + onBack(); + void cancelBuilderlabLogin().catch(() => { + // The modal has already closed and the login attempt is invalidated; + // cancellation is best-effort cleanup for the native/browser flow. }); }; @@ -115,6 +148,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { setAuth(null); setIdentity(null); setCommunities([]); + setShowCreate(false); setName(""); setAvailability(null); }); @@ -188,6 +222,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { normalizedName.length <= 63 && VALID_HOSTED_COMMUNITY_NAME.test(normalizedName); const atCommunityLimit = communities.length >= HOSTED_COMMUNITY_LIMIT; + const hasCommunities = activeCommunities.length > 0; React.useEffect(() => { if (!identity || identityMismatch || !normalizedName || !validName) { @@ -228,6 +263,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { if ( !onboarding.start({ source: "first-community", + firstCommunityPage: "owned", relayUrl, communityName: community.name ?? community.slug, }) @@ -268,231 +304,442 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { }; const busy = action !== null; + // The account is set up once we're signed in with a linked, matching + // identity. Until then the sign-in / link-identity modal drives the flow and + // the page behind it shows a blurred preview of where communities will land. + const ready = Boolean(auth && identity && !identityMismatch); + const modalOpen = !loading && !ready; - return ( -
-

Your communities

-

- Sign in to connect a community you already own on this machine, or - create a new one. -

+ const errorBox = error ? ( +
+ + + + + {error} + +
+ ) : null; + + const creationFeedback = atCommunityLimit + ? `You’ve reached the limit of ${HOSTED_COMMUNITY_LIMIT} hosted communities.` + : name && !validName + ? "Use lowercase letters, numbers, and single hyphens." + : checkingName + ? "Checking availability…" + : availability === false + ? "That address is already taken." + : availability === true + ? "That address is available." + : null; -
- {error ? ( + // The composed `.` line renders at text-4xl, but a valid name + // can be up to 63 chars and the suffix adds another 21 — far wider than the + // card at the 800px app minimum. Scale the font down to fit the container + // (container-query width) while capping at text-4xl (2.25rem) so short names + // keep the full-size treatment and long names stay fully visible instead of + // overflowing the surface. + const composedAddressLength = + (name ? name.length : "your-community".length) + + HOSTED_COMMUNITY_SUFFIX.length + + 1; // leading dot before the suffix + // ~0.62em is the monospace glyph advance; 90cqw leaves a safety margin so the + // glyphs never touch the card edge. + const addressFontSize = `min(2.25rem, calc(90cqw / ${( + composedAddressLength * 0.62 + ).toFixed(2)}))`; + + const creationInput = (inline: boolean) => ( + { + setName(event.target.value.toLowerCase()); + setAvailability(null); + }} + placeholder={inline ? "Community name here" : "your-community"} + spellCheck={false} + style={ + inline + ? undefined + : { + width: `${name ? name.length : "your-community".length}ch`, + fontSize: addressFontSize, + } + } + value={name} + /> + ); + + const renderCreationForm = (inline: boolean) => + inline ? ( +
+
+ + {creationInput(true)} +
+
+ ) : ( + +
- - {error} + {creationInput(false)} + + .{HOSTED_COMMUNITY_SUFFIX} +
- ) : null} +
+
+ ); + + return ( +
+

+ {hasCommunities ? "Choose a community" : "Create a community"} +

+

+ {hasCommunities + ? "Connect one you own, or start something new." + : "Claim a Buzz address to get started."} +

+
{loading ? (
- Checking Builderlab sign-in + Checking sign-in
- ) : !auth ? ( -
-

Sign in or create an account

-

- Builderlab provides Block-hosted Buzz communities. Authentication - opens in your browser and returns here. -

- {action === "Signing in…" ? ( -
-
- - Waiting for your browser… -
- -
+ ) : ready ? ( + <> + {errorBox} + {hasCommunities ? ( + <> + +
+

+ Your communities +

+
    + {activeCommunities.map((community, index) => ( +
  • +
    +

    + {community.name ?? + community.slug ?? + "Hosted community"} +

    +

    + {community.normalized_host} +

    +
    + +
  • + ))} +
+ + {!showCreate ? ( + +

+ Want to create a new community? +

+ +
+ ) : ( + + {renderCreationForm(true)} + + )} +
+
+
+

+ {creationFeedback ?? "Community address status"} +

+ ) : ( - + <> + {renderCreationForm(false)} +

+ {creationFeedback ?? "Community address status"} +

+ )} -
- ) : !identity ? ( -
-

Connect this Buzz identity

-

- Link this device’s Buzz key to{" "} - {auth.email ?? auth.name ?? "your Builderlab account"}. Buzz signs - a one-time challenge locally; your private key never leaves - Desktop. -

- -
- ) : identityMismatch ? ( -
-
- -
-

- This account uses a different Buzz identity -

-

- This Builderlab account is connected to another Buzz identity. - You can disconnect that identity and reconnect this device, or - sign out to use a different email. + + ) : ( + + )} +

+ + {!modalOpen ? ( + + + + + ) : null} + + { + if (open) return; + if (action === "Signing in…") { + cancelSignInAndGoBack(); + return; + } + if (!busy) goBack(); + }} + > + +
+ + + {!auth ? ( + <> + + Set up your community + + + Sign in to connect a community you already own or create a new + one. We’ll open Builderlab in your browser, then bring you + back to Buzz. + + {errorBox ? ( +
{errorBox}
+ ) : null} + {action === "Signing in…" ? ( + + ) : ( + + )} + {/* Quiet breadcrumb: Buzz itself is open source; this hosted + relay is the one account-backed piece of the flow. */} +

+ Buzz is open source. Builderlab hosts the relay for this + account.

-

+ + ) : !identity ? ( + <> + + Finish connecting Buzz + + + Your Builderlab account + {auth.email ? ` (${auth.email})` : ""} is ready. Connect this + device’s Buzz identity to finish setup. Your private key stays + on this device. + + {errorBox ? ( +

{errorBox}
+ ) : null} + + + ) : ( + <> + + This account uses a different Buzz identity + + + This account is connected to another Buzz identity. Reconnect + this device, or sign out to use a different email. + +

Account: {identity.npub ?? boundPubkey}
This device: {localNpub ?? localPubkey}

-
+ {errorBox ? ( +
{errorBox}
+ ) : null} +
-
-
-
- ) : ( - <> -
- - Signed in{auth.email ? ` as ${auth.email}` : ""} with this Buzz - identity -
- - {activeCommunities.length > 0 ? ( -
-

Connect to one you own

-
    - {activeCommunities.map((community, index) => ( -
  • -
    -

    - {community.name ?? - community.slug ?? - "Hosted community"} -

    -

    - {community.normalized_host} -

    -
    - -
  • - ))} -
-
- ) : null} - -
-

Create a new community

-

- Choose the address your team will use to connect. -

-
- { - setName(event.target.value.toLowerCase()); - setAvailability(null); - }} - placeholder="north-star" - spellCheck={false} - value={name} - /> - - .{HOSTED_COMMUNITY_SUFFIX} - -
- {atCommunityLimit ? ( -

- You’ve reached the limit of {HOSTED_COMMUNITY_LIMIT} hosted - communities. -

- ) : name && !validName ? ( -

- Use lowercase letters, numbers, and single hyphens. -

- ) : checkingName ? ( -

- Checking availability… -

- ) : availability === false ? ( -

- That address is already taken. -

- ) : availability === true ? ( -

- That address is available. -

- ) : null} - -
- - )} -
- - - - + + )} +
+ +
); } diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index 8952c5d948..33edf14bb2 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -98,6 +98,7 @@ export function WelcomeSetup({ setRelayUrlError(null); communityOnboarding.start({ source: "first-community", + firstCommunityPage: "join", relayUrl: normalizedRelayUrl, }); }, @@ -108,6 +109,7 @@ export function WelcomeSetup({ (relayWsUrl: string, code: string, policyReceipt?: string) => { communityOnboarding.start({ source: "first-community", + firstCommunityPage: "join", relayUrl: relayWsUrl, inviteCode: code, policyReceipt, diff --git a/desktop/src/features/onboarding/communityOnboarding.tsx b/desktop/src/features/onboarding/communityOnboarding.tsx index 1f0fe45ec7..e2215a79f8 100644 --- a/desktop/src/features/onboarding/communityOnboarding.tsx +++ b/desktop/src/features/onboarding/communityOnboarding.tsx @@ -26,9 +26,13 @@ export type CommunityOnboardingStage = */ | "entering"; +export type FirstCommunityPage = "join" | "owned"; + export type CommunityOnboardingTransaction = { id: string; source: CommunityOnboardingSource; + /** First-run screen that launched this transaction, restored on cancel. */ + firstCommunityPage?: FirstCommunityPage; stage: CommunityOnboardingStage; relayUrl: string; inviteCode?: string; @@ -68,6 +72,7 @@ export type CommunityOnboardingTransactionPatch = Partial< export type StartCommunityOnboardingInput = { source: CommunityOnboardingSource; + firstCommunityPage?: FirstCommunityPage; relayUrl: string; inviteCode?: string; communityName?: string; @@ -150,6 +155,8 @@ export function startCommunityOnboarding( if (existing?.relayUrl === relayUrl) { const updated = { ...existing, + firstCommunityPage: + input.firstCommunityPage ?? existing.firstCommunityPage, inviteCode: input.inviteCode?.trim() || existing.inviteCode, communityName: input.communityName?.trim() || existing.communityName, token: input.token?.trim() || existing.token, @@ -169,6 +176,7 @@ export function startCommunityOnboarding( const transaction: CommunityOnboardingTransaction = { id: crypto.randomUUID(), source: input.source, + firstCommunityPage: input.firstCommunityPage, stage: input.inviteCode?.trim() ? "claiming" : "connecting", relayUrl, inviteCode: input.inviteCode?.trim() || undefined, diff --git a/desktop/src/shared/styles/globals/theme.css b/desktop/src/shared/styles/globals/theme.css index 9add81c03f..634dc78ffa 100644 --- a/desktop/src/shared/styles/globals/theme.css +++ b/desktop/src/shared/styles/globals/theme.css @@ -53,6 +53,16 @@ --buzz-gradient-dark-top: #4a4616; --buzz-gradient-dark-bottom: #0a1423; --buzz-content-dark: 0 0% 10.1960784314%; + /* Hosted-community onboarding intentionally keeps this light palette in + both app themes; semantic tokens keep component code color-agnostic. */ + --buzz-hosted-community-surface-fg: 23 23 23; + --buzz-hosted-community-divider-border: 150 150 10; + --buzz-hosted-community-action-bg: 240 240 205; + --buzz-hosted-community-action-bg-hover: 232 232 188; + --buzz-hosted-community-input-bg: 255 255 255; + --buzz-hosted-community-modal-action-fg: 255 255 255; + --buzz-hosted-community-modal-overlay-bg: 0 0 0; + --buzz-hosted-community-identity-bg: 255 255 255; } .dark { diff --git a/desktop/src/shared/ui/dialog.tsx b/desktop/src/shared/ui/dialog.tsx index d0b1fe5a34..7da86b69d8 100644 --- a/desktop/src/shared/ui/dialog.tsx +++ b/desktop/src/shared/ui/dialog.tsx @@ -45,6 +45,8 @@ type DialogContentProps = React.ComponentPropsWithoutRef< > & { /** Extra classes for the built-in close button (e.g. a themed icon color). */ closeButtonClassName?: string; + /** Extra classes for this dialog's backdrop. */ + overlayClassName?: string; overlayVariant?: "default" | "transparent"; showCloseButton?: boolean; /** @@ -66,6 +68,7 @@ const DialogContent = React.forwardRef< className, children, closeButtonClassName, + overlayClassName, overlayVariant = "default", showCloseButton = true, surface = "default", @@ -76,11 +79,12 @@ const DialogContent = React.forwardRef<
+ page.evaluate(() => + window.localStorage.getItem("buzz-community-onboarding-transaction.v1"), + ), + ) + .toBeNull(); }); test("first-community owner can create and connect a hosted community", async ({ @@ -829,19 +846,46 @@ test("first-community owner can create and connect a hosted community", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect( - page.getByRole("heading", { name: "Connect this Buzz identity" }), + page.getByRole("heading", { name: "Finish connecting Buzz" }), ).toBeVisible(); - await page - .getByRole("button", { name: "Connect this Buzz identity" }) - .click(); - await expect(page.getByText("Signed in as owner@example.com")).toBeVisible(); - await page - .getByRole("textbox", { name: "Community address" }) - .fill("bee-lab"); - await expect(page.getByText("That address is available.")).toBeVisible(); - await page.getByRole("button", { name: "Create and connect" }).click(); + await page.getByRole("button", { name: "Connect and continue" }).click(); + const createSurface = page.getByTestId("hosted-community-create-surface"); + const surfaceBoxBeforeFeedback = await createSurface.boundingBox(); + const communityNameInput = page.getByTestId("hosted-community-address-input"); + await communityNameInput.fill("bee-lab"); + await expect(communityNameInput).toHaveAttribute("style", /width: 7ch;/); + const availabilityFeedback = page.getByText("That address is available."); + await expect(availabilityFeedback).toBeVisible(); + const [feedbackBox, surfaceBox, inputBox, suffixBox] = await Promise.all([ + availabilityFeedback.boundingBox(), + createSurface.boundingBox(), + page.getByTestId("hosted-community-address-input").boundingBox(), + page.locator("#hosted-community-suffix").boundingBox(), + ]); + if ( + !surfaceBoxBeforeFeedback || + !feedbackBox || + !surfaceBox || + !inputBox || + !suffixBox + ) { + throw new Error("Could not measure hosted community creation layout"); + } + expect(surfaceBox.y).toBe(surfaceBoxBeforeFeedback.y); + expect(surfaceBox.height).toBe(surfaceBoxBeforeFeedback.height); + const addressLeft = inputBox.x; + const addressRight = suffixBox.x + suffixBox.width; + expect( + Math.abs( + (addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2), + ), + ).toBeLessThanOrEqual(1); + expect(feedbackBox.y).toBeGreaterThanOrEqual( + surfaceBox.y + surfaceBox.height, + ); + await page.getByRole("button", { name: "Next" }).click(); await expect( page.getByRole("heading", { name: "Build your profile" }), ).toBeVisible(); @@ -854,6 +898,69 @@ test("first-community owner can create and connect a hosted community", async ({ .toContain("wss://bee-lab.communities.buzz.xyz"); }); +test("hosted community address line stays within the card for a long name", async ({ + page, +}) => { + await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); + await page.addInitScript((pubkey) => { + window.localStorage.setItem( + `buzz-machine-onboarding-complete.v2:${pubkey}`, + "true", + ); + }, BLANK_TYLER_IDENTITY.pubkey); + await installMockBridge( + page, + {}, + { + relayWsUrl: "wss://default.example.com", + skipOnboardingSeed: true, + skipCommunitySeed: true, + }, + ); + // The 800px app minimum is the worst case for the full-width address line. + await page.setViewportSize({ width: 800, height: 720 }); + await page.goto("/"); + + await page + .getByRole("button", { name: "Create or connect to my own community" }) + .click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); + await expect( + page.getByRole("heading", { name: "Finish connecting Buzz" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Connect and continue" }).click(); + + const createSurface = page.getByTestId("hosted-community-create-surface"); + const communityNameInput = page.getByTestId("hosted-community-address-input"); + // A maximum-length (63 char) valid name — the overflow case Wes flagged; the + // 7-char check above cannot catch it. + const longName = "a".repeat(63); + await communityNameInput.fill(longName); + await expect(communityNameInput).toHaveValue(longName); + + const [surfaceBox, inputBox, suffixBox] = await Promise.all([ + createSurface.boundingBox(), + communityNameInput.boundingBox(), + page.locator("#hosted-community-suffix").boundingBox(), + ]); + if (!surfaceBox || !inputBox || !suffixBox) { + throw new Error("Could not measure hosted community creation layout"); + } + const addressLeft = inputBox.x; + const addressRight = suffixBox.x + suffixBox.width; + // The composed `.` line must stay within the card — no + // horizontal overflow past the surface or the 800px window. + expect(addressLeft).toBeGreaterThanOrEqual(surfaceBox.x); + expect(addressRight).toBeLessThanOrEqual(surfaceBox.x + surfaceBox.width); + expect(addressRight).toBeLessThanOrEqual(800); + // …and it stays centered within the card. + expect( + Math.abs( + (addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2), + ), + ).toBeLessThanOrEqual(2); +}); + test("first-community reports a created community without a relay address", async ({ page, }) => { @@ -888,11 +995,9 @@ test("first-community reports a created community without a relay address", asyn await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page - .getByRole("textbox", { name: "Community address" }) - .fill("bee-lab"); + await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab"); await expect(page.getByText("That address is available.")).toBeVisible(); - await page.getByRole("button", { name: "Create and connect" }).click(); + await page.getByRole("button", { name: "Next" }).click(); await expect(page.getByRole("alert")).toContainText( "The community was created, but Builderlab did not return its relay address.", ); @@ -901,9 +1006,7 @@ test("first-community reports a created community without a relay address", asyn ).toHaveCount(0); }); -test("first-community can cancel a closed-browser sign-in and retry", async ({ - page, -}) => { +test("first-community X cancels a pending sign-in", async ({ page }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await page.addInitScript((pubkey) => { window.localStorage.setItem( @@ -925,24 +1028,18 @@ test("first-community can cancel a closed-browser sign-in and retry", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect(page.getByText("Waiting for your browser…")).toBeVisible(); - await page.getByRole("button", { name: "Cancel sign-in" }).click(); await expect( - page.getByRole("button", { name: "Continue with Builderlab" }), + page.getByRole("button", { name: "Cancel sign-in" }), + ).toHaveCount(0); + await page.getByRole("button", { name: "Close" }).click(); + await expect( + page.getByRole("button", { name: "Create or connect to my own community" }), ).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) .toEqual(expect.arrayContaining(["cancel_builderlab_login"])); - - await page.evaluate(() => { - if (window.__BUZZ_E2E_CONFIG__?.mock) - window.__BUZZ_E2E_CONFIG__.mock.builderlabLoginDelayMs = 0; - }); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); - await expect( - page.getByRole("heading", { name: "Connect this Buzz identity" }), - ).toBeVisible(); }); test("first-community owner can replace a mismatched account identity", async ({ @@ -984,7 +1081,7 @@ test("first-community owner can replace a mismatched account identity", async ({ .getByRole("button", { name: "Use this device's identity" }) .click(); await expect( - page.getByText("Signed in as old-owner@example.com"), + page.getByRole("textbox", { name: "Community name" }), ).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) @@ -1036,7 +1133,7 @@ test("first-community explains when the local identity belongs to another accoun ), ).toBeVisible(); await expect( - page.getByRole("button", { name: "Connect this Buzz identity" }), + page.getByRole("heading", { name: "Finish connecting Buzz" }), ).toBeVisible(); }); @@ -1074,9 +1171,7 @@ test("back clears Builderlab auth before returning to first-community choices", await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await expect( - page.getByRole("button", { name: "Continue with Builderlab" }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); }); test("first-community shows the scenario cards for localhost", async ({