mirror of
https://github.com/priyanshujain/margin-mail.git
synced 2026-10-02 19:17:05 +00:00
build the app, and ship it on Linux and Windows as well as macOS
The client itself: the sync engine and mirror, the Gmail and IMAP providers, the screener, the three boxes and two piles, compose and send, the sanitiser and tracker stripping, contacts, clips, backup, notifications and the guide. The pipeline that ships it. Three runners now build in parallel and the publish gate wants all four platform keys in latest.json before a release leaves draft. Linux gets two more packages Tauri does not build: a flatpak repackaged from the deb against GNOME 48, and a Nix package that relinks the deb against nixpkgs so it runs as a native Wayland client rather than through Xwayland. CI runs the Rust suite on all three desktops rather than on Linux alone, and rebuilds the flatpak manifest on every push to main. Two things that were only ever exercised on macOS and were wrong everywhere else. The menu bar was built by adjusting the submenus macOS is given, so on a platform whose default menu has no File or View the new File menu landed after Edit, Check for Updates landed nowhere, and the three places and the reading pane had no menu at all. The deb declared libwebkit2gtk-4.1-0 and libgtk-3-0 twice, because tauri.conf.json named the dependencies Tauri already emits. The updater key exists now, so releases can sign their artifacts.
This commit is contained in:
1 parent
088ec9c6e4
commit
eb59cb5f8d
423 files changed
+93217
-251
No files matched your search
@@ -0,0 +1,131 @@
|
||||
// Search, which is not a place you go to but a thing that takes the stage and gives it back.
|
||||
//
|
||||
// The two assertions worth making are the ones a store on its own cannot promise: that the results
|
||||
// behave like a list (the pane still opens a thread from them) and that Escape puts back exactly
|
||||
// what was there, scroll and selection included.
|
||||
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { checkedRows, listReady, listScroll, MIDDAY, openApp, rows, settle } from "./app";
|
||||
|
||||
const shots = join(fileURLToPath(new URL("..", import.meta.url)), "screenshots");
|
||||
|
||||
const senders = (page: import("@playwright/test").Page) =>
|
||||
rows(page).then((all) => all.map((row) => row.sender));
|
||||
|
||||
test("/ focuses search, and Escape gives the Inbox back with its scroll and its selection", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Shorter than the suite's default on purpose. What is being proved is that Escape puts the
|
||||
// scroll back, which needs a list with somewhere to scroll to, and tying that to the number of
|
||||
// rows that happen to fit at the current row height is how this test broke when the row height
|
||||
// changed.
|
||||
await page.setViewportSize({ width: 1440, height: 560 });
|
||||
await openApp(page, { now: MIDDAY() });
|
||||
await listReady(page);
|
||||
|
||||
await page.keyboard.press("j");
|
||||
await page.keyboard.press("x");
|
||||
await page.locator(".list").evaluate((el) => el.scrollTo(0, el.scrollHeight));
|
||||
await settle(page);
|
||||
const parked = await listScroll(page);
|
||||
expect(parked).toBeGreaterThan(0);
|
||||
|
||||
await page.keyboard.press("/");
|
||||
expect(await page.evaluate(() => document.activeElement?.className)).toBe("search-input");
|
||||
|
||||
await page.locator(".search-input").fill("lisbon");
|
||||
await expect(page.locator(".list-title")).toHaveText("Search");
|
||||
await expect.poll(() => senders(page)).toEqual(["Airbnb"]);
|
||||
await expect(page.locator(".search-note")).toHaveText(
|
||||
"Searching what is on this device. Older mail is on Gmail.",
|
||||
);
|
||||
|
||||
// The pane works as usual over the results.
|
||||
await page.locator(".row").first().click();
|
||||
await expect(page.locator(".thread-subject")).toHaveText("Your reservation in Lisbon is confirmed");
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.locator(".list-title")).toHaveText("Inbox");
|
||||
await expect.poll(() => listScroll(page)).toBeGreaterThan(parked - 4);
|
||||
expect(await listScroll(page)).toBeLessThan(parked + 4);
|
||||
expect(await checkedRows(page)).toEqual(["Karthik Rao"]);
|
||||
});
|
||||
|
||||
test("search reaches spam, and the row says which place it came out of", async ({ page }) => {
|
||||
await openApp(page, { now: MIDDAY() });
|
||||
await listReady(page);
|
||||
|
||||
// The message you most need to find is the one something else decided you should not see, so a
|
||||
// search with no `in:` in it reaches spam. Nothing in the Inbox would have found this.
|
||||
await page.keyboard.press("/");
|
||||
await page.locator(".search-input").fill("verification");
|
||||
await expect.poll(() => senders(page)).toEqual(["Aćcount Security"]);
|
||||
await expect(page.locator(".row .row-mark[title='Spam']")).toHaveCount(1);
|
||||
|
||||
// And `in:` narrows to one place when that is what was meant.
|
||||
await page.locator(".search-input").fill("notice in:trash");
|
||||
await expect.poll(() => senders(page)).toEqual(["Parcel Notice"]);
|
||||
await expect(page.locator(".row .row-mark[title='Trash']")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("an operator in the query reads as one", async ({ page }) => {
|
||||
await openApp(page, { now: MIDDAY() });
|
||||
await listReady(page);
|
||||
|
||||
await page.keyboard.press("/");
|
||||
await page.locator(".search-input").fill("from:maya dinner");
|
||||
await expect.poll(() => senders(page)).toEqual(["Maya Raghunathan"]);
|
||||
|
||||
// The query is printed at the head of its results, with the operator drawn as one.
|
||||
await expect(page.locator(".search-terms .pill")).toHaveText("from:maya");
|
||||
await expect(page.locator(".search-terms .search-word")).toHaveText("dinner");
|
||||
});
|
||||
|
||||
test("the foot of a short answer offers the provider's search", async ({ page }) => {
|
||||
await openApp(page, { now: MIDDAY() });
|
||||
await listReady(page);
|
||||
|
||||
await page.keyboard.press("/");
|
||||
await page.locator(".search-input").fill("lease");
|
||||
await expect.poll(() => senders(page)).not.toEqual([]);
|
||||
|
||||
const older = page.locator(".search-foot .button", { hasText: "Search older mail on Gmail" });
|
||||
await expect(older).toBeVisible();
|
||||
await older.click();
|
||||
|
||||
// Asking the provider replaces the note with what came back, and the offer is not made twice.
|
||||
await expect(page.locator(".search-note")).toContainText("from Gmail");
|
||||
await expect(older).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("the field says it is looking until the answer lands", async ({ page }) => {
|
||||
await openApp(page, { now: MIDDAY(), storage: { "marginmail-dev-pending": "1" } });
|
||||
await listReady(page);
|
||||
|
||||
await page.keyboard.press("/");
|
||||
await expect(page.locator(".search")).toHaveAttribute("data-phase", "idle");
|
||||
await page.locator(".search-input").fill("lisbon");
|
||||
// The Inbox's rows are still under the Search title for most of a second, and the phase on the
|
||||
// field is the one thing that says they are not the answer yet.
|
||||
await expect(page.locator(".search")).toHaveAttribute("data-phase", "searching");
|
||||
await expect.poll(() => senders(page)).toEqual(["Airbnb"]);
|
||||
await expect(page.locator(".search")).toHaveAttribute("data-phase", "idle");
|
||||
});
|
||||
|
||||
test("search results", async ({ page }) => {
|
||||
mkdirSync(shots, { recursive: true });
|
||||
await openApp(page, { now: MIDDAY() });
|
||||
await listReady(page);
|
||||
|
||||
await page.keyboard.press("/");
|
||||
await page.locator(".search-input").fill("from:arun lease");
|
||||
await expect.poll(() => senders(page)).toEqual(["Arun Kulkarni"]);
|
||||
await page.locator(".row").first().click();
|
||||
await expect(page.locator(".thread-subject")).toBeVisible();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await settle(page);
|
||||
await page.screenshot({ path: join(shots, "search.png") });
|
||||
});
|
||||
Reference in new issue
Block a user