mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
423 lines
19 KiB
TypeScript
423 lines
19 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { installDesktopBridge } from "./bridge";
|
|
import { completeSupabaseSignIn } from "./auth";
|
|
|
|
test("settings opens permissions and the carousel banner adapts to each page", 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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).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('img[src$="login-banner.png"]')).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
banner
|
|
.locator('img[src$="login-banner.png"]')
|
|
.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.getByText("test@email.com", { exact: true })).toBeVisible();
|
|
const loginHeading = (await tour.getByRole("heading", { level: 2 }).boundingBox())!;
|
|
const brand = tour.locator("[data-onboarding-brand]");
|
|
const brandBounds = (await brand.boundingBox())!;
|
|
const accountBannerBounds = (await banner.boundingBox())!;
|
|
expect(accountBannerBounds.height).toBeGreaterThan(bannerBounds.height);
|
|
expect(accountBannerBounds.width).toBe(bannerBounds.width);
|
|
expect(brandBounds.y).toBeGreaterThanOrEqual(
|
|
accountBannerBounds.y + accountBannerBounds.height,
|
|
);
|
|
expect(brandBounds.y + brandBounds.height).toBeLessThan(loginHeading.y);
|
|
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(permissionHeading.x).toBe(loginHeading.x);
|
|
const permissionBrandBounds = (await brand.boundingBox())!;
|
|
expect(permissionBrandBounds.y + permissionBrandBounds.height).toBeLessThan(
|
|
permissionHeading.y,
|
|
);
|
|
expect(permissionBrandBounds.width).toBe(brandBounds.width);
|
|
expect(permissionBrandBounds.height).toBe(brandBounds.height);
|
|
const navigationBounds = (await pages.boundingBox())!;
|
|
expect(navigationBounds.y).toBeGreaterThan(startBounds.y + startBounds.height);
|
|
expect(
|
|
await 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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).click();
|
|
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
|
|
});
|
|
|
|
test("first launch persists completion and signed-in users can continue", async ({ page }) => {
|
|
await installDesktopBridge(page);
|
|
await page.addInitScript(() => {
|
|
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.getByRole("button", { name: "Continue", exact: true }).click();
|
|
await expect(tour.getByRole("heading", { name: "Allow Recordly to record" })).toBeVisible();
|
|
await expect(tour.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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).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] img[src$="login-banner.png"]'),
|
|
).toBeInViewport();
|
|
for (const size of [
|
|
{ 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 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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).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
|
|
.poll(() => body.evaluate((element) => element.scrollHeight - element.clientHeight))
|
|
.toBeLessThanOrEqual(1);
|
|
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(tour.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] img[src$="login-banner.png"]'),
|
|
).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();
|
|
});
|
|
|
|
test("sign-in is mandatory even after onboarding was completed", async ({ page }) => {
|
|
await installDesktopBridge(page, "preview.mp4", { signedIn: false });
|
|
await page.addInitScript(() => sessionStorage.setItem("recordly.demo-session", "1"));
|
|
await page.goto("/?windowType=editor");
|
|
const tour = page.getByRole("dialog", { name: "Welcome to Recordly" });
|
|
await expect(tour).toHaveAttribute("data-onboarding-state", "login");
|
|
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).toBeInViewport();
|
|
await expect(pages.getByRole("button", { name: "Account", exact: true })).toHaveAttribute(
|
|
"aria-current",
|
|
"page",
|
|
);
|
|
await expect(pages.getByRole("button", { name: "Permissions", exact: true })).toBeDisabled();
|
|
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).toHaveAttribute("data-onboarding-state", "login");
|
|
});
|
|
|
|
test("Supabase email callback advances to permissions only after a session is returned", async ({
|
|
page,
|
|
}) => {
|
|
await installDesktopBridge(page, "preview.mp4", { signedIn: false });
|
|
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 magic link" }).click();
|
|
expect((await otpRequest).postDataJSON()).toMatchObject({
|
|
email: "test@email.com",
|
|
create_user: true,
|
|
});
|
|
await expect(tour.getByText(/Check your inbox to verify your email/)).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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).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(tour.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(tour.locator("[data-permissions-confetti]")).toBeVisible();
|
|
await expect(tour.locator("[data-permissions-confetti]")).toHaveCount(0, { timeout: 5000 });
|
|
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
|
|
await expect(tour.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(tour.locator("[data-permissions-confetti]")).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
tour
|
|
.locator("[data-permissions-confetti] span")
|
|
.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 page.addInitScript((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 page.getByRole("radio", { name: "Settings", exact: true }).click();
|
|
await page.getByRole("button", { name: "Show onboarding" }).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");
|
|
});
|
|
|
|
test("a permission request opens its page even after onboarding was already completed", async ({
|
|
page,
|
|
}) => {
|
|
await installDesktopBridge(page);
|
|
await page.addInitScript(() => {
|
|
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: "Welcome to Recordly" });
|
|
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 });
|
|
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");
|
|
});
|
|
}
|