mirror of
https://github.com/priyanshujain/margin-calendar.git
synced 2026-10-02 11:07:04 +00:00
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.
137 lines
4.9 KiB
TypeScript
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);
|
|
});
|