Files
pj d4c3a304b5 Sign in on a phone with no console work, in the browser's own session
Mobile OAuth reused the desktop client all along; what stopped it was the
browser. Sending the user out to Safari or Chrome backgrounds the app, iOS
suspends it, and the redirect carrying the code arrives at a socket nobody
is accepting on. The consent page now opens in front of the app instead, in
SFSafariViewController or a Chrome Custom Tab, so the loopback listener
stays live and the existing `installed` client is enough. Verified against
Google's real consent screen on a simulator and an emulator.

A per-platform client is still supported and is now an upgrade rather than a
prerequisite. On iOS it buys ASWebAuthenticationSession, which shares
Safari's session so nobody is asked to sign in to Google twice. Android
needs nothing: Custom Tabs share Chrome's cookies, measured rather than
assumed. iOS session sharing could not be confirmed on the simulator and
wants a real device.

Never an app-owned WebView: Google blocks it, and rightly, since a webview
the app controls can read the password typed into it.

Cancelling is no longer reported as a failure. AuthEvent carries a
`cancelled` flag, set by comparing against the constant every back-out path
returns, and Google's `access_denied` on desktop counts too.

Five frontend bugs found by driving the real UI, not by reading it: the
details card slid under the tab bar leaving its buttons unhittable; the
ghost click after a touch pressed a button in the card that tap had just
opened, opening the editor by itself; the swipe that pages the day was dead
over every read-only block; 84px of macOS traffic-light lane was reserved on
platforms with no traffic lights; and the desktop header ignored the top
safe area on an iPad. A first launch now says what to do next rather than
showing an empty grid, and accounts are named as Google accounts throughout.
2026-08-12 18:32:45 +05:30

137 lines
4.9 KiB
TypeScript

// The overlays: summoned, then dismissed. Nothing here is resident, so the thing to check is that
// each one arrives, takes the keyboard while it is up, and goes away in the right order.
import { expect, test, type Page } from "@playwright/test";
import { openApp, settle } from "./app";
async function openCalendars(page: Page) {
await page.locator('button[title="Calendars"]').click();
}
async function openSettings(page: Page) {
await page.keyboard.press("ControlOrMeta+,");
}
const PANELS = [
{ name: "Jump to", open: (page: Page) => page.keyboard.press("m"), contains: /Today/ },
{ name: "Calendars", open: openCalendars, contains: /you@example\.com/ },
{ name: "Settings", open: openSettings, contains: /Week view/ },
{
name: "Google accounts",
open: async (page: Page) => {
await openSettings(page);
await page.getByRole("button", { name: "Manage" }).click();
},
contains: /Connect a Google account/,
},
];
for (const panel of PANELS) {
test(`${panel.name} opens and closes on Escape`, async ({ page }) => {
await openApp(page);
await panel.open(page);
const dialog = page.getByRole("dialog", { name: panel.name });
await expect(dialog).toBeVisible();
await expect(dialog).toContainText(panel.contains);
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
// The grid is still there underneath, and still fitted.
await expect(page.locator(".grid-event").first()).toBeVisible();
});
test(`${panel.name} takes the keyboard while it is open`, async ({ page }) => {
await openApp(page);
await panel.open(page);
await expect(page.getByRole("dialog", { name: panel.name })).toBeVisible();
// `d` is day view out there. In here it belongs to the panel, which does nothing with it.
await page.keyboard.press("d");
await settle(page);
expect(await page.evaluate(() => document.documentElement.getAttribute("data-view"))).toBe(
"week",
);
await expect(page.getByRole("dialog", { name: panel.name })).toBeVisible();
});
}
test("clicking the backdrop dismisses a panel", async ({ page }) => {
await openApp(page);
await openCalendars(page);
const dialog = page.getByRole("dialog", { name: "Calendars" });
await expect(dialog).toBeVisible();
await page.mouse.click(30, 500);
await expect(dialog).toHaveCount(0);
});
test("Escape unwinds one layer at a time", async ({ page }) => {
await openApp(page);
await openSettings(page);
await page.getByRole("button", { name: "Manage" }).click();
const accounts = page.getByRole("dialog", { name: "Google accounts" });
await expect(accounts).toBeVisible();
await page.getByRole("button", { name: "Disconnect" }).first().click();
const confirm = page.locator(".confirm");
await expect(confirm).toBeVisible();
await expect(confirm).toContainText("Disconnect [email protected]?");
// The confirmation goes first, and the panel it belongs to is still there behind it.
await page.keyboard.press("Escape");
await expect(confirm).toHaveCount(0);
await expect(accounts).toBeVisible();
await expect(page.getByRole("button", { name: "Disconnect" }).first()).toBeVisible();
await page.keyboard.press("Escape");
await expect(accounts).toHaveCount(0);
});
test("the mini month jumps the view to the day you pick", async ({ page }) => {
await openApp(page);
const range = await page.locator(".header-range").textContent();
await page.keyboard.press("m");
await expect(page.getByRole("dialog", { name: "Jump to" })).toBeVisible();
// A day in the same month that is not the one already showing.
await page.locator(".mini-day:not([data-outside]):not([data-span])").last().click();
await expect(page.getByRole("dialog", { name: "Jump to" })).toHaveCount(0);
expect(await page.locator(".header-range").textContent()).not.toBe(range);
await expect(page.locator(".grid-event").first()).toBeVisible();
});
test("turning a calendar off takes its events off the grid", async ({ page }) => {
await openApp(page);
const meetup = page.locator(".grid-event", { hasText: "Antithesis" });
expect(await meetup.count()).toBeGreaterThan(0);
await openCalendars(page);
await page
.locator(".cal-row", { hasText: "Antithesis x Bengaluru Systems Meetup" })
.locator("input[type=checkbox]")
.uncheck();
await page.keyboard.press("Escape");
await expect(meetup).toHaveCount(0);
});
test("the theme switch actually repaints", async ({ page }) => {
await openApp(page);
const paper = () =>
page.evaluate(() => getComputedStyle(document.querySelector(".grid")!).backgroundColor);
const light = await paper();
await openSettings(page);
await page.getByRole("button", { name: "Dark", exact: true }).click();
await page.keyboard.press("Escape");
await settle(page);
expect(await page.evaluate(() => document.documentElement.getAttribute("data-theme"))).toBe(
"dark",
);
expect(await paper()).not.toBe(light);
});