Files
Recordly/tests/ui/onboarding.spec.ts
webadderall cabde58de0 Fix Recordly 2.0 part 7 follow-up review findings
Preserve existing PR history and integrate validated second-cycle fixes.
2026-11-08 08:52:14 +11:00

754 lines
33 KiB
TypeScript

import { expect, type Page, test } from "@playwright/test";
import { completeSupabaseSignIn } from "./auth";
import { productionTest } from "./production";
import { installDesktopBridge, installDesktopBridgeOverrides } from "./bridge";
async function openDevSettings(page: Page) {
await page.getByRole("button", { name: "Recordly account", exact: true }).click();
await page
.getByRole("dialog", { name: "Account menu", exact: true })
.getByRole("button", { name: "Settings", exact: true })
.click();
await page
.getByRole("dialog", { name: "Settings", exact: true })
.getByRole("tab", { name: "Dev", exact: true })
.click();
}
test("dedicated onboarding route fills its own window", async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false });
await page.setViewportSize({ width: 760, height: 760 });
await page.goto("/?windowType=onboarding");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour).toBeVisible();
await expect(page.locator("[data-startup-intro]")).toHaveCount(0);
await expect(page.locator("html")).toHaveAttribute("data-window-type", "onboarding");
await expect(tour.locator("[data-onboarding-window-drag]")).toBeVisible();
await expect(tour.getByRole("button", { name: "Close onboarding window" })).toBeVisible();
await expect
.poll(async () => {
const bounds = await tour.boundingBox();
return (
bounds &&
bounds.x >= -1 &&
bounds.y >= -1 &&
bounds.width >= 758 &&
bounds.height >= 758
);
})
.toBe(true);
await tour.getByRole("button", { name: "Close onboarding window" }).click();
await expect(page.locator("html")).toHaveAttribute("data-onboarding-closed", "true");
});
test("Show onboarding opens directly after onboarding was completed", async ({ page }) => {
await installDesktopBridge(page);
await installDesktopBridgeOverrides(page, () => {
window.electronAPI.getAppSetting = (key) =>
key === "recordly.onboarding.v1.seen" ? true : null;
window.electronAPI.showOnboardingWindow = async (options) => {
document.documentElement.dataset.previewRequested = String(options?.preview);
return false;
};
});
await page.goto("/?windowType=editor");
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
await expect(page.locator("[data-startup-intro]")).toHaveCount(0);
await expect(page.locator("html")).toHaveAttribute("data-preview-requested", "true");
await expect(page.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
});
test("settings opens permissions and both pages keep the same layout", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await installDesktopBridge(page);
await page.goto("/?windowType=editor");
await expect(page.getByRole("button", { name: "Home", exact: true })).toBeVisible({
timeout: 15000,
});
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(tour.getByText("Allowed", { exact: true })).toHaveCount(2);
await expect
.poll(() =>
tour
.locator(".onboarding-permissions")
.evaluate((element) => element.scrollHeight - element.clientHeight),
)
.toBeLessThanOrEqual(1);
const pages = tour.getByRole("navigation", { name: "Onboarding pages" });
await expect(pages.getByRole("button", { name: "Permissions", exact: true })).toHaveAttribute(
"aria-current",
"page",
);
await expect(pages.getByRole("button")).toHaveCount(2);
await expect(tour.getByRole("listitem")).toHaveCount(0);
await expect(pages).toBeInViewport();
await expect(page.locator(".modal__container")).not.toHaveAttribute("data-entering", "true");
const permissionBounds = (await tour.boundingBox())!;
const banner = tour.locator("[data-onboarding-artwork]");
const bannerBounds = (await banner.boundingBox())!;
await expect(banner.locator("[data-recordly-demo]")).toBeVisible();
await expect
.poll(() =>
banner
.locator("[data-recordly-demo] .recordly-banner-light")
.evaluate((image: HTMLImageElement) => image.naturalWidth),
)
.toBeGreaterThan(0);
const startBounds = (await tour
.getByRole("button", { name: "Start recording", exact: true })
.boundingBox())!;
await expect(tour.getByRole("button", { name: "Continue", exact: true })).toHaveCount(0);
await pages.getByRole("button", { name: "Account", exact: true }).click();
await expect(tour).toHaveAttribute("data-onboarding-state", "login");
await expect(pages.getByRole("button", { name: "Account", exact: true })).toHaveAttribute(
"aria-current",
"page",
);
await expect(tour.getByPlaceholder("you@example.com")).toBeVisible();
const loginHeading = (await tour.getByRole("heading", { level: 2 }).boundingBox())!;
await expect(tour.locator("[data-onboarding-brand]")).toHaveCount(0);
const accountBannerBounds = (await banner.boundingBox())!;
expect(accountBannerBounds.height).toBeGreaterThanOrEqual(64);
expect(accountBannerBounds.width).toBe(bannerBounds.width);
await expect(banner.locator('img[src$="recordly-128.png"]')).toHaveCount(0);
expect(await tour.boundingBox()).toEqual(permissionBounds);
expect(await banner.boundingBox()).toEqual(accountBannerBounds);
await page.screenshot({ path: "test-results/onboarding-login.png", animations: "disabled" });
await pages.getByRole("button", { name: "Permissions", exact: true }).focus();
await page.keyboard.press("Enter");
await expect(tour).toHaveAttribute("data-onboarding-state", "permissions");
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
expect(await tour.boundingBox()).toEqual(permissionBounds);
expect(await banner.boundingBox()).toEqual(bannerBounds);
const permissionHeading = (await tour.getByRole("heading", { level: 2 }).boundingBox())!;
expect(loginHeading.x + loginHeading.width / 2).toBeCloseTo(
permissionBounds.x + permissionBounds.width / 2,
0,
);
const titleLogo = tour
.getByRole("heading", { name: "Allow Recordly to record" })
.locator(".onboarding-title-logo");
await expect(titleLogo).toHaveCount(1);
const titleLogoBounds = (await titleLogo.boundingBox())!;
expect(titleLogoBounds.y).toBeGreaterThanOrEqual(permissionHeading.y - 1);
expect(titleLogoBounds.y + titleLogoBounds.height).toBeLessThanOrEqual(
permissionHeading.y + permissionHeading.height + 1,
);
await expect(tour.locator("[data-onboarding-brand]")).toHaveCount(0);
const navigationBounds = (await pages.boundingBox())!;
expect(navigationBounds.y).toBeGreaterThan(startBounds.y + startBounds.height);
await expect
.poll(() =>
tour.getByRole("button", { name: "Start recording", exact: true }).boundingBox(),
)
.toEqual(startBounds);
await tour.getByRole("button", { name: "Start recording", exact: true }).click();
await expect(tour).not.toBeVisible();
await expect(page.locator("html")).toHaveAttribute("data-hud-opened", "true");
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
});
productionTest(
"first launch opens permissions directly for signed-in users and persists completion",
async ({ page }) => {
await installDesktopBridge(page);
await installDesktopBridgeOverrides(page, () => {
window.electronAPI.getAppSetting = (key) =>
JSON.parse(localStorage.getItem(key) || "null");
window.electronAPI.setAppSetting = (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
return true;
};
});
await page.goto("/?windowType=editor");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour).toBeVisible({ timeout: 15000 });
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(page.locator("[data-permissions-confetti]")).toBeVisible();
await tour.getByRole("button", { name: "Start recording", exact: true }).click();
await expect
.poll(() => page.evaluate(() => localStorage.getItem("recordly.onboarding.v1.seen")))
.toBe("true");
await page.reload();
await expect(page.getByRole("button", { name: "Home", exact: true })).toBeVisible({
timeout: 15000,
});
await expect(tour).not.toBeVisible();
},
);
test("onboarding fits a compact window with reduced motion", async ({ page }) => {
await installDesktopBridge(page);
await page.setViewportSize({ width: 620, height: 740 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/?windowType=editor");
await expect(page.getByRole("button", { name: "Home", exact: true })).toBeVisible({
timeout: 15000,
});
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(
tour.getByRole("button", { name: "Start recording", exact: true }),
).toBeInViewport();
await expect(tour.locator("[data-onboarding-artwork] [data-recordly-demo]")).toBeInViewport();
for (const size of [
{ width: 450, height: 450 },
{ width: 390, height: 390 },
{ width: 720, height: 780 },
{ width: 640, height: 650 },
{ width: 390, height: 600 },
{ width: 620, height: 740 },
]) {
await page.setViewportSize(size);
await expect
.poll(() =>
tour
.locator(".onboarding-permissions")
.evaluate((element) => element.scrollHeight - element.clientHeight),
)
.toBeLessThanOrEqual(1);
await expect(
tour.getByRole("button", { name: "Start recording", exact: true }),
).toBeInViewport();
await expect(tour.getByRole("navigation", { name: "Onboarding pages" })).toBeInViewport();
}
await page.screenshot({ path: "test-results/onboarding-compact.png" });
await tour.getByRole("button", { name: "Back to account" }).click();
await expect(tour).toHaveAttribute("data-onboarding-state", "login");
});
test("short dark windows keep the banner and navigation visible while the account form scrolls", async ({
page,
}) => {
await installDesktopBridge(page);
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/?windowType=editor");
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
await page.setViewportSize({ width: 390, height: 600 });
await page.evaluate(() => document.documentElement.classList.add("dark"));
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(
tour.getByRole("button", { name: "Start recording", exact: true }),
).toBeInViewport();
const body = tour.locator("[data-onboarding-body]");
await expect(body).toHaveCSS("overflow-y", "auto");
await expect
.poll(() =>
body.evaluate(
(element) =>
element.parentElement!.scrollHeight - element.parentElement!.clientHeight,
),
)
.toBeLessThanOrEqual(1);
const bodyBounds = (await body.boundingBox())!;
const footerBounds = (await tour
.getByRole("button", { name: "Back to account" })
.boundingBox())!;
expect(bodyBounds.y + bodyBounds.height).toBeLessThanOrEqual(footerBounds.y);
await expect(page.locator("[data-permissions-confetti]")).toHaveCount(0);
await page.screenshot({ path: "test-results/onboarding-narrow-permissions.png" });
await tour.getByRole("button", { name: "Back to account" }).click();
const bounds = await tour.boundingBox();
await expect(tour.getByRole("button", { name: "Continue", exact: true })).toBeInViewport();
await expect(tour.locator("[data-onboarding-artwork] [data-recordly-demo]")).toBeInViewport();
expect(await tour.boundingBox()).toEqual(bounds);
await expect(tour).toHaveJSProperty("scrollWidth", bounds!.width);
await page.screenshot({ path: "test-results/onboarding-narrow-account.png" });
await tour.getByRole("button", { name: "Continue", exact: true }).click();
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
});
productionTest(
"square onboarding uses consistent typography and scrolls permissions above fixed actions",
async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false });
await page.setViewportSize({ width: 450, height: 450 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/?windowType=onboarding");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
const typography = () =>
tour.getByRole("heading", { level: 2 }).evaluate((element) => {
const heading = getComputedStyle(element);
const logo = element.querySelector<HTMLImageElement>(".onboarding-title-logo")!;
const header = element.closest<HTMLElement>(".onboarding-header")!;
return {
fontFamily: heading.fontFamily,
fontSize: heading.fontSize,
fontWeight: heading.fontWeight,
lineHeight: heading.lineHeight,
letterSpacing: heading.letterSpacing,
logoWidth: logo.getBoundingClientRect().width,
logoHeight: logo.getBoundingClientRect().height,
headerGap: getComputedStyle(header).gap,
};
});
await expect(tour.getByRole("heading", { name: "Welcome to Recordly" })).toBeVisible();
const accountTypography = await typography();
const accountBanner = await tour.locator("[data-onboarding-artwork]").boundingBox();
await tour.getByPlaceholder("you@example.com").fill("test@email.com");
await tour.getByRole("button", { name: "Email me a code" }).click();
await expect(tour.getByText(/Enter code sent to/)).toBeVisible();
await completeSupabaseSignIn(page);
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
expect(await typography()).toEqual(accountTypography);
const permissionBanner = (await tour.locator("[data-onboarding-artwork]").boundingBox())!;
// The banner grows into spare space, so its height can change when the page content does.
expect(permissionBanner.width).toBe(accountBanner!.width);
expect(permissionBanner.height).toBeGreaterThanOrEqual(64);
const body = tour.locator("[data-onboarding-body]");
await expect(body).toHaveCSS("overflow-y", "auto");
await expect
.poll(() => body.evaluate((element) => element.scrollHeight - element.clientHeight))
.toBeGreaterThan(0);
await body.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
await expect(
tour.getByText("Microphone and camera access are requested when you use them.", {
exact: false,
}),
).toBeInViewport();
await expect(tour.getByRole("button", { name: "Start recording" })).toBeInViewport();
await expect(tour.getByRole("navigation", { name: "Onboarding pages" })).toBeInViewport();
const bodyBounds = (await body.boundingBox())!;
const footerBounds = (await tour.locator(".onboarding-footer").boundingBox())!;
expect(bodyBounds.y + bodyBounds.height).toBeLessThanOrEqual(footerBounds.y);
await page.screenshot({ path: "test-results/onboarding-square-permissions.png" });
},
);
productionTest("sign-in is mandatory even after onboarding was completed", async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: true });
await page.addInitScript(() => sessionStorage.setItem("recordly.demo-session", "1"));
await page.goto("/?windowType=editor");
const tour = page.getByRole("dialog", { name: "Sign in to Recordly", exact: true });
await expect(tour).toBeVisible();
await expect(tour.getByRole("button", { name: "Continue without signing in" })).toHaveCount(0);
await expect(tour.getByText("Optional", { exact: true })).toHaveCount(0);
await expect(tour.getByText("Sign in to continue", { exact: true })).toHaveCount(0);
const pages = tour.getByRole("navigation", { name: "Onboarding pages" });
await expect(pages).toHaveCount(0);
await expect(tour.getByRole("button", { name: "Start recording" })).toHaveCount(0);
await expect(tour.getByRole("button", { name: "Close", exact: true })).toHaveCount(0);
await page.keyboard.press("Escape");
await page.mouse.click(5, 5);
await expect(tour).toBeVisible();
await page.reload();
await expect(tour).toBeVisible();
});
productionTest(
"Supabase email callback advances to permissions only after a session is returned",
async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false });
await installDesktopBridgeOverrides(page, () => {
window.electronAPI.getAppSetting = (key) =>
JSON.parse(localStorage.getItem(key) || "null");
window.electronAPI.setAppSetting = (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
return true;
};
});
await page.goto("/?windowType=editor");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await tour.getByPlaceholder("you@example.com").fill("test@email.com");
const otpRequest = page.waitForRequest((request) => request.url().includes("/auth/v1/otp"));
await tour.getByRole("button", { name: "Email me a code" }).click();
expect((await otpRequest).postDataJSON()).toMatchObject({
email: "test@email.com",
create_user: true,
});
await expect(tour.getByText(/Enter code sent to/)).toBeVisible();
await expect(tour).toHaveAttribute("data-onboarding-state", "login");
const exchange = page.waitForRequest((request) => request.url().includes("/auth/v1/token"));
await completeSupabaseSignIn(page);
expect((await exchange).postDataJSON()).toMatchObject({
auth_code: "ui-auth-code",
code_verifier: expect.any(String),
});
await expect(tour).toHaveAttribute("data-onboarding-state", "permissions");
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(
tour
.getByRole("navigation", { name: "Onboarding pages" })
.getByRole("button", { name: "Permissions", exact: true }),
).toBeEnabled();
await expect(
tour.getByRole("button", { name: "Start recording", exact: true }),
).toBeEnabled();
await tour.getByRole("button", { name: "Start recording", exact: true }).click();
await expect(tour).not.toBeVisible();
await page.reload();
await expect(page.getByRole("button", { name: "Home", exact: true })).toBeVisible({
timeout: 15000,
});
await expect(tour).not.toBeVisible();
},
);
test("missing macOS permissions gate Start recording until access is allowed", async ({ page }) => {
await installDesktopBridge(page);
await page.addInitScript(() => {
let screen = false;
let accessibility = false;
Object.assign(window.electronAPI, {
getScreenRecordingPermissionStatus: async () => ({
success: true,
status: screen ? "granted" : "denied",
}),
getAccessibilityPermissionStatus: async () => ({
success: true,
trusted: accessibility,
prompted: false,
}),
openScreenRecordingPreferences: async () => {
screen = true;
document.documentElement.dataset.screenSettings = "true";
return { success: true };
},
openAccessibilityPreferences: async () => {
accessibility = true;
document.documentElement.dataset.accessibilitySettings = "true";
return { success: true };
},
});
});
await page.goto("/?windowType=editor");
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
const start = tour.getByRole("button", { name: "Start recording", exact: true });
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(start).toBeDisabled();
await page.setViewportSize({ width: 390, height: 600 });
await expect
.poll(() =>
tour
.locator(".onboarding-permissions")
.evaluate((element) => element.scrollHeight - element.clientHeight),
)
.toBeLessThanOrEqual(1);
await expect(page.locator("[data-permissions-confetti]")).toHaveCount(0);
await expect(page.locator("html")).not.toHaveAttribute("data-screen-settings");
await tour.getByRole("button", { name: "Allow Screen Recording", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-screen-settings", "true");
await expect(start).toBeDisabled();
await tour.getByRole("button", { name: "Allow Accessibility", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-accessibility-settings", "true");
await expect(tour.getByText("Allowed", { exact: true })).toHaveCount(2);
await expect(start).toBeEnabled();
await expect(page.locator("[data-permissions-confetti]")).toBeVisible();
await expect(page.locator("[data-permissions-confetti]")).toHaveCount(0, { timeout: 5000 });
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await expect(page.locator("[data-permissions-confetti]")).toHaveCount(0);
await tour.getByRole("button", { name: "Back to account" }).click();
await tour.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator("[data-permissions-confetti]")).toBeVisible();
await expect
.poll(() =>
page
.locator("[data-permissions-confetti] canvas")
.first()
.evaluate((element) => Number(getComputedStyle(element).opacity)),
)
.toBeGreaterThan(0.5);
await page.screenshot({
path: "test-results/onboarding-permissions.png",
animations: "disabled",
});
await start.click();
await expect(tour).not.toBeVisible();
await expect(page.locator("html")).toHaveAttribute("data-hud-opened", "true");
});
for (const platform of ["win32", "linux"]) {
test(`${platform} onboarding skips macOS permission APIs`, async ({ page }) => {
await installDesktopBridge(page);
await installDesktopBridgeOverrides(
page,
(platform) => {
window.electronAPI.getPlatform = async () => platform;
window.electronAPI.getScreenRecordingPermissionStatus = async () => {
throw new Error("macOS-only check");
};
window.electronAPI.getAccessibilityPermissionStatus = async () => {
throw new Error("macOS-only check");
};
},
platform,
);
await page.goto("/?windowType=editor");
await openDevSettings(page);
await page.getByRole("button", { name: "Show onboarding" }).click();
await expect(
page.getByRole("heading", { name: "Welcome to Recordly", exact: true }),
).toBeVisible();
await page
.getByRole("dialog", { name: "Welcome to Recordly" })
.getByRole("button", { name: "Continue", exact: true })
.click();
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(
tour.getByRole("button", { name: "Start recording", exact: true }),
).toBeEnabled();
await expect(tour.getByText("Screen Recording", { exact: true })).toHaveCount(0);
await expect(tour.getByText("macOS System Settings", { exact: false })).toHaveCount(0);
await tour.getByRole("button", { name: "Start recording", exact: true }).click();
await expect(tour).not.toBeVisible();
await expect(page.locator("html")).toHaveAttribute("data-hud-opened", "true");
});
}
test("HUD routes missing permissions to onboarding without opening System Settings on launch", async ({
page,
}) => {
await installDesktopBridge(page);
await page.addInitScript(() => {
window.electronAPI.getScreenRecordingPermissionStatus = async () => ({
success: true,
status: "denied",
});
window.electronAPI.openScreenRecordingPreferences = async () => {
document.documentElement.dataset.settingsOpened = "true";
return { success: true };
};
});
await page.goto("/?windowType=hud-overlay");
await expect(page.locator("html")).toHaveAttribute("data-permissions-opened", "true");
await expect(page.locator("html")).not.toHaveAttribute("data-settings-opened");
});
productionTest(
"a permission request opens its page even after onboarding was already completed",
async ({ page }) => {
await installDesktopBridge(page);
await installDesktopBridgeOverrides(page, () => {
window.electronAPI.getAppSetting = (key) =>
key === "recordly.onboarding.v1.seen" ||
key === "recordly.onboarding.permissionsRequested"
? true
: null;
window.electronAPI.getScreenRecordingPermissionStatus = async () => ({
success: true,
status: "denied",
});
});
await page.goto("/?windowType=editor");
const tour = page.getByRole("dialog", { name: "Permissions", exact: true });
await expect(tour).toBeVisible({ timeout: 15000 });
await expect(tour.getByRole("navigation")).toHaveCount(0);
await expect(tour.getByRole("button", { name: "Back to account" })).toHaveCount(0);
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
await expect(
tour.getByRole("button", { name: "Continue without signing in" }),
).not.toBeVisible();
},
);
for (const platform of ["darwin", "win32", "linux"]) {
test(`${platform} HUD requires a Supabase session before recording`, async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false });
await page.addInitScript((platform) => {
window.electronAPI.getPlatform = async () => platform;
}, platform);
await page.goto("/?windowType=hud-overlay");
await expect(page.locator("html")).toHaveAttribute("data-permissions-opened", "true");
});
}
for (const signedIn of [true, false]) {
test(`dev preview replays welcome and music with signedIn=${signedIn}`, async ({ page }) => {
await installDesktopBridge(page, "preview.mp4", { signedIn, onboarded: true });
await page.addInitScript(() => {
localStorage.setItem("recordly.first-boot-music.v1.played", "true");
window.electronAPI.getAppSetting = (key) =>
key === "recordly.onboarding.v1.seen" ||
key === "recordly.first-boot-music.v1.played"
? true
: null;
window.electronAPI.setAppSetting = (key, value) => {
localStorage.setItem(`test-write:${key}`, JSON.stringify(value));
return true;
};
HTMLMediaElement.prototype.play = async function () {
if (this instanceof HTMLAudioElement)
document.documentElement.dataset.welcomeMusic = this.src;
};
HTMLMediaElement.prototype.pause = function () {
if (this instanceof HTMLAudioElement)
document.documentElement.dataset.welcomeMusicStopped = "true";
};
});
await page.goto("/?windowType=onboarding&onboardingPreview=1");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
await expect(tour).toHaveAttribute("data-onboarding-state", "login");
await expect(tour.getByRole("heading", { name: "Welcome to Recordly" })).toBeVisible();
await expect(tour.getByPlaceholder("you@example.com")).toHaveCount(signedIn ? 0 : 1);
if (signedIn) await expect(tour.getByText("test@email.com", { exact: true })).toBeVisible();
await expect(tour.getByText("Your account", { exact: true })).toHaveCount(0);
await expect(page.locator("html")).toHaveAttribute(
"data-welcome-music",
/audio\/first-boot.m4a$/,
);
const music = tour.getByRole("button", { name: "Mute welcome music" });
await expect(music).toHaveAttribute("aria-pressed", "true");
const soundBounds = await music.boundingBox();
expect(soundBounds).not.toBeNull();
expect(soundBounds!.x).toBeLessThan(64);
expect(soundBounds!.y).toBeLessThan(64);
await page.screenshot({
path: `test-results/onboarding-sound-toggle-${signedIn}.png`,
animations: "disabled",
});
await music.click();
const pausedMusic = tour.getByRole("button", { name: "Unmute welcome music" });
await expect(pausedMusic).toBeVisible();
await expect(pausedMusic).toHaveAttribute("aria-pressed", "false");
await expect(page.locator("html")).toHaveAttribute("data-welcome-music-stopped", "true");
await pausedMusic.click();
await expect(music).toHaveAttribute("aria-pressed", "true");
await tour.getByRole("button", { name: "Continue", exact: true }).click();
await expect(tour).toHaveAttribute("data-onboarding-state", "permissions");
await tour.getByRole("button", { name: "Back to account" }).click();
await tour
.getByRole("navigation")
.getByRole("button", { name: "Permissions", exact: true })
.click();
await expect(tour).toHaveAttribute("data-onboarding-state", "permissions");
await tour.getByRole("button", { name: "Start recording", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-hud-opened", "true");
await expect(page.locator("html")).toHaveAttribute("data-onboarding-closed", "true");
await expect(page.locator("html")).toHaveAttribute("data-welcome-music-stopped", "true");
expect(
await page.evaluate(() =>
localStorage.getItem("test-write:recordly.onboarding.v1.seen"),
),
).toBeNull();
expect(
await page.evaluate(() =>
localStorage.getItem("test-write:recordly.first-boot-music.v1.played"),
),
).toBeNull();
await page.reload();
await expect(tour.getByRole("heading", { name: "Welcome to Recordly" })).toBeVisible();
await expect(tour.getByRole("button", { name: "Mute welcome music" })).toBeVisible();
});
}
test("Review permission setup uses onboarding preview in development", async ({ page }) => {
await installDesktopBridge(page);
await installDesktopBridgeOverrides(page, () => {
window.electronAPI.showOnboardingWindow = async (options) => {
document.documentElement.dataset.previewRequested = String(options?.preview);
return false;
};
});
await page.goto("/?windowType=editor");
await page.getByRole("button", { name: "Recordly account", exact: true }).click();
await page
.getByRole("dialog", { name: "Account menu", exact: true })
.getByRole("button", { name: "Settings", exact: true })
.click();
await page
.getByRole("dialog", { name: "Settings", exact: true })
.getByRole("tab", { name: "Recording", exact: true })
.click();
await page.getByRole("button", { name: "Review permission setup", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-preview-requested", "true");
const tour = page.getByRole("dialog", { name: "Welcome to Recordly", exact: true });
await expect(tour.getByPlaceholder("you@example.com")).toHaveCount(0);
await expect(tour.getByText("test@email.com", { exact: true })).toBeVisible();
await tour.getByRole("button", { name: "Continue", exact: true }).click();
await expect(tour.getByRole("navigation")).toBeVisible();
await expect(tour.getByRole("button", { name: "Back to account" })).toBeVisible();
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
});
test("welcome mark and video blend into floating blue blobs regardless of reduced motion", async ({
page,
}) => {
await installDesktopBridge(page, "preview.mp4", { signedIn: false, onboarded: false });
await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" });
await page.setViewportSize({ width: 760, height: 760 });
await page.goto("/?windowType=onboarding&onboardingPreview=1");
await expect(page.getByRole("heading", { name: "Welcome to Recordly" })).toBeVisible();
const styles = await page.locator(".onboarding-title-logo").evaluate((logo) => {
const style = getComputedStyle(logo);
const root = document.querySelector(".onboarding-layout")!;
const pulse = getComputedStyle(root.querySelector(".onboarding-blob")!);
const artwork = getComputedStyle(document.querySelector(".onboarding-artwork")!);
return {
color: style.backgroundColor,
mask: style.maskImage,
pulse: pulse.animationName,
duration: pulse.animationDuration,
fade: artwork.maskImage,
};
});
expect(styles.color).not.toBe("rgba(0, 0, 0, 0)");
expect(styles.mask).toContain("recordly-mark.svg");
expect(styles.pulse).toBe("onboarding-blob-one");
expect(styles.duration).toBe("12s");
expect(styles.fade).toContain("rgba(0, 0, 0, 0) 100%");
await expect(page.locator(".onboarding-blob")).toHaveCount(3);
const position = () =>
page.locator(".onboarding-blob-one").evaluate((el) => el.getBoundingClientRect().x);
const initialPosition = await position();
await expect
.poll(async () => Math.abs((await position()) - initialPosition), { timeout: 6000 })
.toBeGreaterThan(8);
await page.screenshot({ path: test.info().outputPath("welcome-pulse-preview.png") });
});