Add split login screen and local demo account, refine settings and navigation

This commit is contained in:
webadderall
2026-09-23 13:15:01 +10:00
parent 898eea9a73
commit bebe00778d
21 changed files with 394 additions and 186 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+189 -135
View File
@@ -1,5 +1,7 @@
import { demoLoginEnabled } from "@/lib/auth/demoSession";
import { WindowsLogo } from "@phosphor-icons/react";
import { useI18n } from "@/contexts/I18nContext";
import { GoogleLogo, SignOut, XLogo } from "@/components/ui/icons";
import { GoogleLogo, SignOut } from "@/components/ui/icons";
import type { User } from "@supabase/supabase-js";
import { type FormEvent, useEffect, useState } from "react";
import {
@@ -43,9 +45,7 @@ function friendlyAuthError(
if (action === "google") {
return t("editor.cloud.googleUnavailable");
}
if (action === "x") {
return t("editor.cloud.xUnavailable");
}
if (action === "azure") return "Microsoft sign-in is not available yet.";
return t("editor.cloud.providerUnavailable");
}
return message;
@@ -95,8 +95,11 @@ export function RecordlySignInDialog({
const submitEmail = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!configured || busy) return;
if ((!configured && !demoLoginEnabled) || busy) return;
void run("email", async () => {
if (!configured && email.trim().toLowerCase() !== "test@email.com") {
throw new Error("Email sign-in is not available yet.");
}
await signInWithEmail(email.trim(), password);
onAuthenticated();
});
@@ -114,139 +117,190 @@ export function RecordlySignInDialog({
});
};
const disabled = !configured || Boolean(busy);
const disabled = Boolean(busy);
const expanded = email.trim().length > 0;
const artwork = `${import.meta.env.BASE_URL}wallpapers/wallpaper1.jpg`;
return (
<Modal isOpen={open} onOpenChange={onOpenChange}>
<Modal.Backdrop>
<Modal.Container size="sm" placement="center">
<Modal.Dialog>
<Modal.CloseTrigger aria-label={t("common.actions.close")} />
<Modal.Header>
<Modal.Heading>
{user
? t("editor.cloud.accountHeading")
: t("editor.cloud.signInHeading")}
</Modal.Heading>
<Description>
{user
? user.email
: reason === "share"
? t("editor.cloud.signInShareDescription")
: t("editor.cloud.signInDescription")}
</Description>
</Modal.Header>
<Modal.Body className="flex flex-col gap-4">
{user ? (
<Button
variant="secondary"
className="w-full"
isDisabled={Boolean(busy)}
onPress={() => void run("signout", signOutRecordly)}
>
<SignOut className="size-4" />
{busy === "signout"
? t("editor.cloud.signingOut")
: t("editor.cloud.signOut")}
</Button>
) : (
<>
<div className="grid grid-cols-2 gap-3">
<Button
variant="secondary"
className="w-full"
isDisabled={disabled}
onPress={() =>
void run("google", () => signInWithSocial("google"))
}
<Modal.Backdrop
className="bg-cover bg-center"
style={{
backgroundImage: `linear-gradient(#10102066, #10102066), url(${artwork})`,
}}
>
<Modal.Container size="cover" placement="center" className="p-4 sm:p-8">
<Modal.Dialog className="grid h-[min(760px,calc(100dvh-64px))] min-h-0 w-full max-w-[1120px] grid-cols-1 gap-0 overflow-hidden rounded-[32px] p-2 md:grid-cols-2">
<Modal.CloseTrigger
aria-label={t("common.actions.close")}
className="z-20"
/>
<div className="flex min-h-0 items-center justify-center overflow-y-auto px-6 py-10 sm:px-10">
<div className="my-auto w-full max-w-[340px] space-y-7">
<Modal.Header className="items-center text-center">
<Modal.Heading className="text-4xl font-semibold tracking-tight">
{user ? "Your account" : "Welcome back"}
</Modal.Heading>
<Description className="text-sm">
{user
? user.email
: reason === "share"
? "Sign in to share your recordings."
: "Sign in to your Recordly account."}
</Description>
</Modal.Header>
{user ? (
<Button
variant="secondary"
className="w-full"
isDisabled={disabled}
onPress={() => void run("signout", signOutRecordly)}
>
<SignOut className="size-4" />
{busy === "signout"
? t("editor.cloud.signingOut")
: t("editor.cloud.signOut")}
</Button>
) : (
<>
<div className="grid grid-cols-2 gap-3">
<Button
variant="secondary"
className="h-11 w-full rounded-xl"
isDisabled={disabled || !configured}
onPress={() =>
void run("google", () =>
signInWithSocial("google"),
)
}
>
<GoogleLogo className="size-5" />
Google
</Button>
<Button
variant="secondary"
className="h-11 w-full rounded-xl"
isDisabled={disabled || !configured}
onPress={() =>
void run("azure", () =>
signInWithSocial("azure"),
)
}
>
<WindowsLogo className="size-5" />
Microsoft
</Button>
</div>
<div className="flex items-center gap-4">
<Separator className="flex-1" />
<span className="text-xs text-muted">
or continue with email
</span>
<Separator className="flex-1" />
</div>
<Form
className="flex flex-col gap-5"
onSubmit={submitEmail}
>
<GoogleLogo className="size-4" />
Google
</Button>
<Button
variant="secondary"
className="w-full"
isDisabled={disabled}
onPress={() =>
void run("x", () => signInWithSocial("twitter"))
}
>
<XLogo className="size-4" />X
</Button>
</div>
<div className="my-1 flex items-center gap-3">
<Separator className="flex-1" />
<span className="text-xs text-muted">
{t("editor.cloud.or")}
</span>
<Separator className="flex-1" />
</div>
<Form className="flex flex-col gap-4" onSubmit={submitEmail}>
<TextField
name="email"
type="email"
value={email}
onChange={setEmail}
isRequired
isDisabled={Boolean(busy)}
>
<Label>{t("editor.cloud.email")}</Label>
<Input
placeholder="you@example.com"
autoComplete="email"
/>
<FieldError />
</TextField>
<TextField
name="password"
type="password"
value={password}
onChange={setPassword}
isRequired
isDisabled={Boolean(busy)}
>
<Label>{t("editor.cloud.password")}</Label>
<Input autoComplete="current-password" />
<FieldError />
</TextField>
<Button
variant="ghost"
size="sm"
className="-mt-2 self-end"
isDisabled={disabled}
onPress={forgotPassword}
>
{t("editor.cloud.forgotPassword")}
</Button>
<Button
type="submit"
className="w-full"
isDisabled={disabled}
>
{busy === "email"
? t("editor.cloud.signingIn")
: t("editor.cloud.signIn")}
</Button>
</Form>
</>
)}
{message || callbackError ? (
<Alert status={resetSent && !callbackError ? "success" : "danger"}>
<Alert.Indicator />
<Alert.Content>
<Alert.Description>
{message || callbackError}
</Alert.Description>
</Alert.Content>
</Alert>
) : null}
</Modal.Body>
{!configured && !user && (
<Modal.Footer>
<Description role="status" className="min-w-0 flex-1">
{t("editor.cloud.unavailable")}
</Description>
</Modal.Footer>
)}
<TextField
name="email"
type="email"
value={email}
onChange={(value) => {
setEmail(value);
setMessage(undefined);
if (!value.trim()) setPassword("");
}}
isRequired
isDisabled={disabled}
>
<Label>Email</Label>
<Input
className="h-12 rounded-xl bg-default/50 shadow-none"
placeholder="you@example.com"
autoComplete="email"
/>
<FieldError />
</TextField>
{expanded && (
<div className="flex flex-col gap-5">
<TextField
name="password"
type="password"
value={password}
onChange={setPassword}
isRequired
isDisabled={disabled}
>
<Label>Password</Label>
<Input
className="h-12 rounded-xl bg-default/50 shadow-none"
autoComplete="current-password"
/>
<FieldError />
</TextField>
{configured && (
<Button
variant="ghost"
size="sm"
className="-mt-3 self-end"
isDisabled={disabled}
onPress={forgotPassword}
>
{t("editor.cloud.forgotPassword")}
</Button>
)}
<Button
type="submit"
className="h-12 w-full rounded-xl"
isDisabled={
disabled ||
(!configured && !demoLoginEnabled)
}
>
{busy === "email"
? t("editor.cloud.signingIn")
: "Sign in"}
</Button>
</div>
)}
</Form>
</>
)}
{message || callbackError ? (
<Alert
status={resetSent && !callbackError ? "success" : "danger"}
>
<Alert.Indicator />
<Alert.Content>
<Alert.Description>
{message || callbackError}
</Alert.Description>
</Alert.Content>
</Alert>
) : null}
{!configured && !demoLoginEnabled && !user && (
<Description role="status">
{t("editor.cloud.unavailable")}
</Description>
)}
</div>
</div>
<div className="relative hidden min-h-0 overflow-hidden rounded-[26px] md:block">
<img
src={artwork}
alt=""
className="absolute inset-0 size-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/65 via-transparent to-black/10" />
<span className="absolute left-8 top-8 text-lg font-semibold tracking-tight text-white">
Recordly
</span>
<p className="absolute bottom-10 left-8 right-8 text-4xl font-light leading-tight tracking-tight text-white">
Make something
<br />
<strong className="font-semibold">worth sharing.</strong>
</p>
</div>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
+10 -2
View File
@@ -1,5 +1,6 @@
import { demoUser, hasDemoSession, subscribeDemoSession } from "@/lib/auth/demoSession";
import type { User } from "@supabase/supabase-js";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import {
completeAuthCallback,
recordlyAuth,
@@ -7,6 +8,7 @@ import {
} from "@/lib/auth/recordlyAuth";
export function useRecordlyAuth() {
const demo = useSyncExternalStore(subscribeDemoSession, hasDemoSession, () => false);
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string>();
const [loading, setLoading] = useState(recordlyAuthConfigured);
@@ -67,5 +69,11 @@ export function useRecordlyAuth() {
};
}, []);
return { user, accessToken, loading, configured: recordlyAuthConfigured, callbackError };
return {
user: demo ? demoUser : user,
accessToken: demo ? undefined : accessToken,
loading,
configured: recordlyAuthConfigured,
callbackError,
};
}
+16 -8
View File
@@ -233,18 +233,26 @@ function MotionPresetCards({
value={activePresetId ?? undefined}
onChange={(value) => onApply(value as CursorMotionPresetId)}
>
<Label className="text-xs font-medium">{title}</Label>
<Label className="text-[13px] font-medium">{title}</Label>
{MOTION_PRESET_ORDER.map((presetId) => (
<Radio key={presetId} value={presetId}>
<Radio
key={presetId}
value={presetId}
className="rounded-xl border border-separator p-3"
>
<Radio.Content>
<Radio.Control>
<Radio.Indicator />
</Radio.Control>
<Label>{tSettings(`effects.motionPresets.${presetId}.label`)}</Label>
<div className="flex min-w-0 flex-col gap-1">
<Label className="text-[13px] font-medium">
{tSettings(`effects.motionPresets.${presetId}.label`)}
</Label>
<Description className="text-xs leading-relaxed">
{tSettings(`effects.motionPresets.${presetId}.description`)}
</Description>
</div>
</Radio.Content>
<Description>
{tSettings(`effects.motionPresets.${presetId}.description`)}
</Description>
</Radio>
))}
</RadioGroup>
@@ -2123,7 +2131,7 @@ export function SettingsPanel({
{whisperModelDownloadStatus === "downloading" ? (
<div className="h-2 overflow-hidden rounded-full bg-foreground/5">
<div
className="h-full rounded-full bg-[#2196f3] transition-all"
className="h-full rounded-full bg-accent transition-all"
style={{ width: `${whisperModelDownloadProgress}%` }}
/>
</div>
@@ -2312,7 +2320,7 @@ export function SettingsPanel({
)}
</SettingsCategory>
<SettingsCategory category="motion">
<section className="flex flex-col gap-3">
<section className="flex flex-col gap-6">
<SettingsRow
title={tSettings(
"effects.autoApplyFreshRecordingZooms",
+1 -1
View File
@@ -17,7 +17,7 @@ export function SettingsRow({
<section
className={stacked ? "flex flex-col gap-3" : "flex items-center justify-between gap-4"}
>
<div className="min-w-0 space-y-1">
<div className="flex min-w-0 flex-col gap-1">
<Label className="text-[13px] font-medium">{title}</Label>
{description && (
<Description className="text-xs leading-relaxed">{description}</Description>
@@ -11,7 +11,7 @@ export type SidebarCard = {
/** Code-only sidebar content. Empty cards or enabled:false hides this area. */
export const sidebarCardConfig: { enabled: boolean; cards: SidebarCard[] } = {
enabled: true,
enabled: false,
cards: [
{
id: "placeholder",
@@ -1,3 +1,4 @@
import { Separator } from "@heroui/react";
import {
House,
FilmStrip,
@@ -108,16 +109,6 @@ export function EditorHeader(props: Props) {
className={`editor-header-start flex min-w-0 items-center gap-1 ${headerLeftControlsPaddingClass}`}
style={{ WebkitAppRegion: "no-drag" } as CSSProperties}
>
<Button
variant="secondary"
size="sm"
className="mr-2 h-9 shrink-0 gap-2"
aria-expanded={props.clipsOpen}
onClick={props.onToggleClips}
>
<FilmStrip weight={props.clipsOpen ? "fill" : "regular"} className="size-4" />
Clips
</Button>
<Button
ref={projectBrowserTriggerRef}
type="button"
@@ -139,7 +130,7 @@ export function EditorHeader(props: Props) {
</span>
<div
className="editor-header-title flex min-w-0 flex-1 items-center"
className="editor-header-title flex min-w-0 items-center"
style={{ WebkitAppRegion: "no-drag" } as CSSProperties}
>
{isEditingProjectName ? (
@@ -193,6 +184,17 @@ export function EditorHeader(props: Props) {
</Button>
)}
</div>
<Separator orientation="vertical" className="mx-3 h-5 shrink-0" />
<Button
variant="secondary"
size="sm"
className="h-9 shrink-0 gap-2"
aria-expanded={props.clipsOpen}
onClick={props.onToggleClips}
>
<FilmStrip weight={props.clipsOpen ? "fill" : "regular"} className="size-4" />
Clips
</Button>
</div>
<div
@@ -326,7 +326,6 @@ export function EditorShell(props: Props) {
<div className="relative z-10 flex min-h-0 flex-1 pt-3">
<EditorSidebar
accountUser={auth.user}
onToggleVideos={() => library.setOpen((open) => !open)}
onAccountClick={() => requestSignIn("account")}
panelContent={
library.open ? <RecordingLibraryPanel library={library} /> : undefined
@@ -1,4 +1,3 @@
import { File } from "@/components/ui/icons";
import { AccountAvatar } from "@/components/ui/account-avatar";
import type { User } from "@supabase/supabase-js";
import { Camera, ClosedCaptioning, Cursor, Gear, FrameCorners } from "@/components/ui/icons";
@@ -20,7 +19,6 @@ import type { EditorEffectSection } from "../types";
type Props = {
accountUser?: User | null;
onToggleVideos: () => void;
panelContent?: ReactNode;
onAccountClick?: () => void;
t: ReturnType<typeof useI18n>["t"];
@@ -32,7 +30,6 @@ type Props = {
export function EditorSidebar({
t,
accountUser,
onToggleVideos,
activeSection,
setActiveSection,
settingsPanelProps,
@@ -80,19 +77,12 @@ export function EditorSidebar({
className="w-full items-center gap-2"
selectionMode="single"
disallowEmptySelection
selectedKeys={[panelContent ? "videos" : activeSection]}
selectedKeys={panelContent ? [] : [activeSection]}
onSelectionChange={(keys) => {
const key = Array.from(keys)[0];
if (key === "videos") onToggleVideos();
else if (key) setActiveSection(key as EditorEffectSection);
if (key) setActiveSection(key as EditorEffectSection);
}}
>
<Tooltip>
<ToggleButton id="videos" variant="ghost" isIconOnly aria-label="Clips">
<File weight={panelContent ? "fill" : "regular"} className="size-5" />
</ToggleButton>
<Tooltip.Content placement="right">Clips</Tooltip.Content>
</Tooltip>
{sections.map((section) => (
<Tooltip key={section.id}>
<ToggleButton
+42
View File
@@ -0,0 +1,42 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
beforeEach(() => {
vi.resetModules();
const values = new Map<string, string>();
vi.stubGlobal("window", new EventTarget());
vi.stubGlobal("sessionStorage", {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
removeItem: (key: string) => values.delete(key),
});
});
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it("notifies local account changes and stores only a session marker", async () => {
vi.stubEnv("DEV", true);
const { setDemoSession, hasDemoSession, subscribeDemoSession } = await import("./demoSession");
const listener = vi.fn();
const unsubscribe = subscribeDemoSession(listener);
expect(hasDemoSession()).toBe(false);
setDemoSession(true);
expect(hasDemoSession()).toBe(true);
expect(sessionStorage.getItem("recordly.demo-session")).toBe("1");
setDemoSession(false);
expect(hasDemoSession()).toBe(false);
expect(listener).toHaveBeenCalledTimes(2);
unsubscribe();
setDemoSession(true);
expect(listener).toHaveBeenCalledTimes(2);
});
it("cannot enable the local demo account in production", async () => {
vi.stubEnv("DEV", false);
sessionStorage.setItem("recordly.demo-session", "1");
const { setDemoSession, hasDemoSession, demoLoginEnabled } = await import("./demoSession");
setDemoSession(true);
expect(demoLoginEnabled).toBe(false);
expect(hasDemoSession()).toBe(false);
});
+27
View File
@@ -0,0 +1,27 @@
import type { User } from "@supabase/supabase-js";
// Temporary local UI account. Never creates a Supabase session or cloud access token.
export const demoLoginEnabled = import.meta.env.DEV;
const key = "recordly.demo-session";
const event = "recordly-demo-session-changed";
export const demoUser: User = {
id: "recordly-local-demo",
email: "test@email.com",
aud: "local-demo",
app_metadata: {},
user_metadata: { full_name: "Test User" },
created_at: "2026-09-23T00:00:00.000Z",
};
export function hasDemoSession() {
return demoLoginEnabled && typeof window !== "undefined" && sessionStorage.getItem(key) === "1";
}
export function setDemoSession(active: boolean) {
if (!demoLoginEnabled) return;
if (active) sessionStorage.setItem(key, "1");
else sessionStorage.removeItem(key);
window.dispatchEvent(new Event(event));
}
export function subscribeDemoSession(listener: () => void) {
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}
+26 -2
View File
@@ -1,16 +1,26 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
const exchange = vi.hoisted(() => vi.fn(async (_code: string) => ({ error: null })));
const oauth = vi.hoisted(() =>
vi.fn(async (_options: unknown) => ({
data: { url: "https://auth.example.test/oauth" },
error: null,
})),
);
vi.mock("@supabase/supabase-js", () => ({
createClient: () => ({ auth: { exchangeCodeForSession: exchange } }),
createClient: () => ({ auth: { exchangeCodeForSession: exchange, signInWithOAuth: oauth } }),
}));
beforeEach(() => {
vi.resetModules();
exchange.mockClear();
oauth.mockClear();
vi.stubEnv("VITE_SUPABASE_URL", "https://auth.example.test");
vi.stubEnv("VITE_SUPABASE_PUBLISHABLE_KEY", "test-key");
});
afterEach(() => vi.unstubAllEnvs());
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it("exchanges a callback once when live and pending delivery overlap", async () => {
const { completeAuthCallback } = await import("./recordlyAuth");
@@ -29,3 +39,17 @@ it("shows provider errors without attempting a code exchange", async () => {
).rejects.toThrow("Sign-in cancelled");
expect(exchange).not.toHaveBeenCalled();
});
it("requests Microsoft's email scope and opens its OAuth URL externally", async () => {
const openExternalUrl = vi.fn(async () => ({ success: true }));
vi.stubGlobal("window", { electronAPI: { openExternalUrl } });
const { signInWithSocial } = await import("./recordlyAuth");
await signInWithSocial("azure");
expect(oauth).toHaveBeenCalledWith(
expect.objectContaining({
provider: "azure",
options: expect.objectContaining({ scopes: "email", skipBrowserRedirect: true }),
}),
);
expect(openExternalUrl).toHaveBeenCalledExactlyOnceWith("https://auth.example.test/oauth");
});
+16 -2
View File
@@ -1,3 +1,4 @@
import { demoLoginEnabled, demoUser, hasDemoSession, setDemoSession } from "./demoSession";
import { createClient, type Provider, type User } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL?.trim();
@@ -29,6 +30,11 @@ function requireAuth() {
}
export async function signInWithEmail(email: string, password: string): Promise<User> {
if (demoLoginEnabled && email.toLowerCase() === "test@email.com") {
if (password !== "1234") throw new Error("Incorrect email or password.");
setDemoSession(true);
return demoUser;
}
const client = requireAuth();
const { data, error } = await client.auth.signInWithPassword({ email, password });
if (error) throw error;
@@ -48,11 +54,15 @@ async function openAuthUrl(url: string | null) {
if (!result.success) throw new Error(result.error || "Could not open the sign-in page.");
}
export async function signInWithSocial(provider: "google" | "twitter"): Promise<void> {
export async function signInWithSocial(provider: "google" | "azure"): Promise<void> {
const client = requireAuth();
const { data, error } = await client.auth.signInWithOAuth({
provider: provider as Provider,
options: { redirectTo: callbackUrl, skipBrowserRedirect: true },
options: {
redirectTo: callbackUrl,
skipBrowserRedirect: true,
scopes: provider === "azure" ? "email" : undefined,
},
});
if (error) throw error;
await openAuthUrl(data.url);
@@ -82,6 +92,10 @@ async function exchangeAuthCallback(url: string): Promise<void> {
}
export async function signOutRecordly(): Promise<void> {
if (hasDemoSession()) {
setDemoSession(false);
return;
}
const client = requireAuth();
const { error } = await client.auth.signOut();
if (error) throw error;
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from "@playwright/test";
import { installDesktopBridge } from "./bridge";
test("split login expands email, rejects incorrect credentials, and persists the local account until sign-out", async ({
page,
}) => {
await installDesktopBridge(page);
await page.goto("/?windowType=editor");
await page.getByRole("button", { name: "Home", exact: true }).click();
await page.getByRole("button", { name: "Sign in", exact: true }).click();
const login = page.getByRole("dialog", { name: "Welcome back", exact: true });
await expect(login.getByRole("button", { name: "Google", exact: true })).toBeVisible();
await expect(login.getByRole("button", { name: "Microsoft", exact: true })).toBeVisible();
await expect(login.getByLabel("Password", { exact: true })).toHaveCount(0);
await page.screenshot({ path: "test-results/login.png", animations: "disabled" });
await login.getByLabel("Email", { exact: true }).fill("test@email.com");
await login.getByLabel("Password", { exact: true }).fill("incorrect");
await login.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(login.getByText("Incorrect email or password.")).toBeVisible();
await login.getByLabel("Password", { exact: true }).fill("1234");
await page.screenshot({ path: "test-results/login-expanded.png", animations: "disabled" });
await page.setViewportSize({ width: 800, height: 600 });
await expect(login.getByRole("button", { name: "Sign in", exact: true })).toBeInViewport();
await login.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(login).not.toBeVisible();
await page.reload();
await page.getByRole("button", { name: "Home", exact: true }).click();
await page.getByRole("button", { name: "test@email.com", exact: true }).click();
const account = page.getByRole("dialog", { name: "Your account", exact: true });
await expect(account.getByText("test@email.com")).toBeVisible();
await account.getByRole("button", { name: "Sign out", exact: true }).click();
await expect(page.getByRole("dialog", { name: "Welcome back" })).toBeVisible();
});
+16 -9
View File
@@ -327,7 +327,7 @@ test("dashboard supports creation sort, independent folders, shared settings and
});
});
await page.goto("/?windowType=editor");
await page.getByRole("radio", { name: "Clips", exact: true }).click();
await page.getByRole("button", { name: "Clips", exact: true }).click();
await expect(page.getByRole("complementary", { name: "Clips" })).toBeVisible();
await page.getByRole("button", { name: "Home", exact: true }).click();
const home = page.getByRole("dialog", { name: "Projects dashboard" });
@@ -390,6 +390,16 @@ test("dashboard supports creation sort, independent folders, shared settings and
await expect(home.getByText("Preview update UI", { exact: true })).toBeVisible();
await home.getByRole("row", { name: "Motion", exact: true }).click();
await expect(home.getByRole("switch", { name: "Connect Zooms" })).toBeVisible();
const motion = home.getByRole("region", { name: "Motion settings", exact: true });
const label = await motion.getByText("Connect Zooms", { exact: true }).boundingBox();
const description = await motion
.getByText("Smooth consecutive zoom regions into a continuous camera move.", {
exact: true,
})
.boundingBox();
expect(description!.y - (label!.y + label!.height)).toBeGreaterThanOrEqual(3);
expect(description!.x).toBe(label!.x);
await page.screenshot({ path: "test-results/settings-motion.png", animations: "disabled" });
await home.getByRole("row", { name: "Recording", exact: true }).click();
await expect(home.getByText("Recordings folder", { exact: true })).toBeVisible();
await home.getByRole("button", { name: "Change folder" }).click();
@@ -435,16 +445,17 @@ test("Solar navigation selection, circular initials, and Raw sources are consist
});
await page.goto("/?windowType=editor");
const scene = page.getByRole("radio", { name: "Scene", exact: true });
const videos = page.getByRole("radio", { name: "Clips", exact: true });
const videos = page.getByRole("button", { name: "Clips", exact: true });
await expect(page.getByRole("radio", { name: "Clips", exact: true })).toHaveCount(0);
await expect(scene).toBeChecked();
await expect(scene.locator("svg")).toHaveAttribute("data-icon-style", "bold");
await videos.click();
await expect(videos).toBeChecked();
await expect(videos).toHaveAttribute("aria-expanded", "true");
await expect(videos.locator("svg")).toHaveAttribute("data-icon-style", "bold");
await expect(scene.locator("svg")).toHaveAttribute("data-icon-style", "linear");
await scene.click();
await expect(scene).toBeChecked();
await expect(videos).not.toBeChecked();
await expect(videos).toHaveAttribute("aria-expanded", "false");
const homeButton = page.getByRole("button", { name: "Home", exact: true });
await expect(homeButton.locator("svg")).toHaveAttribute("data-icon-style", "bold");
await homeButton.click();
@@ -678,12 +689,8 @@ test("sidebar cards, separate Import, and shortcut settings use the dashboard fl
const home = page.getByRole("dialog", { name: "Projects dashboard" });
const sidebar = home.getByRole("complementary", { name: "Library navigation" });
await expect(sidebar.getByRole("button", { name: "Import", exact: true })).toHaveCount(0);
const banner = sidebar.getByRole("img", { name: "Placeholder banner" });
await expect
.poll(() => banner.evaluate((image: HTMLImageElement) => image.naturalWidth))
.toBeGreaterThan(0);
await expect(sidebar.getByRole("img", { name: "Placeholder banner" })).toHaveCount(0);
const settings = sidebar.getByRole("button", { name: "Settings", exact: true });
expect((await banner.boundingBox())!.y).toBeLessThan((await settings.boundingBox())!.y);
const importButton = home.getByRole("button", { name: "Import", exact: true });
const all = home.getByRole("button", { name: "All", exact: true });
expect(
+2 -2
View File
@@ -75,8 +75,8 @@ test("Clips supports selection, remove all, undo and real timeline drag insertio
.toBeGreaterThan(0);
await expect(panel.getByText("first.mp4", { exact: true })).toBeVisible();
const videosButton = page.getByRole("button", { name: "Clips", exact: true });
expect((await videosButton.boundingBox())!.x).toBeLessThan(
(await page.getByRole("button", { name: "Home", exact: true }).boundingBox())!.x,
expect((await videosButton.boundingBox())!.x).toBeGreaterThan(
(await page.getByRole("button", { name: "Rename project", exact: true }).boundingBox())!.x,
);
expect((await panel.boundingBox())!.x).toBeLessThan(100);
await expect(videosButton).toHaveClass(/button--secondary/);