3 Commits
Author SHA1 Message Date
sbstp 279cdb869d cargo fmt
ci/woodpecker/push/e2e Pipeline was successful
ci/woodpecker/push/fmt Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2026-08-01 23:05:37 -04:00
sbstp fbb49bc560 fix flaky e2e test 2026-08-01 23:05:20 -04:00
sbstp 7309971c74 websocket sync e2e test 2026-08-01 23:01:42 -04:00
5 changed files with 44 additions and 25 deletions
+1
View File
@@ -6,3 +6,4 @@
/e2e/node_modules/ /e2e/node_modules/
/e2e/test-results/ /e2e/test-results/
/e2e/playwright-report/ /e2e/playwright-report/
/test-results/
+2 -2
View File
@@ -48,7 +48,7 @@ test("the add-meal picker closes when clicking outside", async ({ page }) => {
const picker = page.locator(".meal-picker-backdrop"); const picker = page.locator(".meal-picker-backdrop");
await expect(picker).toBeVisible(); await expect(picker).toBeVisible();
// Click the backdrop itself (outside the modal card). // Click the backdrop itself (outside the modal card), at the viewport corner.
await picker.click({ position: { x: 5, y: 5 } }); await page.mouse.click(10, 10);
await expect(picker).toHaveCount(0); await expect(picker).toHaveCount(0);
}); });
+31
View File
@@ -0,0 +1,31 @@
import { expect } from "@playwright/test";
import { test } from "../fixtures";
import { registerAndLogin, createList, addItem } from "../helpers";
test("a list updates live for another user via websocket", async ({ page, browser, server }) => {
// User A registers and creates a list.
await registerAndLogin(page, "alice@example.com");
await createList(page, "Weekly shop");
const listUrl = page.url();
// User B registers in a separate context (separate session).
const contextB = await browser.newContext({ baseURL: server.baseURL });
const pageB = await contextB.newPage();
await registerAndLogin(pageB, "bob@example.com");
// Both users open the same list.
await page.goto(listUrl);
await pageB.goto(listUrl);
// Give the websocket connections a moment to establish.
await page.waitForTimeout(500);
// User A adds an item.
await addItem(page, "Apple", "2");
// It should appear on User B's page without any reload.
await expect(pageB.locator(".item-row").filter({ hasText: "Apple" })).toBeVisible();
await expect(pageB.locator(".item-row").filter({ hasText: "Apple" }).locator(".item-qty")).toHaveText("(2)");
await contextB.close();
});
+7 -7
View File
@@ -1,12 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use crate::domain::{ use crate::domain::{DomainError, DomainResult, GroceryList, Item, Meal, SessionUser, User};
DomainError, DomainResult, GroceryList, Item, Meal, SessionUser, User,
};
use crate::ports::{ use crate::ports::{
CategoryRepository, InvitationRepository, ItemRepository, ListRepository, MealIngredientRepository, CategoryRepository, InvitationRepository, ItemRepository, ListRepository,
MealRepository, NewItem, PasswordHasher, RealtimeNotifier, SessionRepository, TokenGenerator, MealIngredientRepository, MealRepository, NewItem, PasswordHasher, RealtimeNotifier,
UserRepository, SessionRepository, TokenGenerator, UserRepository,
}; };
use crate::sqlite::SqliteDatabase; use crate::sqlite::SqliteDatabase;
@@ -312,7 +310,9 @@ impl MealService {
pub async fn create_meal(&self, name: String, description: String) -> DomainResult<Meal> { pub async fn create_meal(&self, name: String, description: String) -> DomainResult<Meal> {
let meals = Arc::clone(&self.meals); let meals = Arc::clone(&self.meals);
self.db self.db
.run(move |txn| Box::pin(async move { meals.create_meal(txn, name, description).await })) .run(move |txn| {
Box::pin(async move { meals.create_meal(txn, name, description).await })
})
.await .await
} }
+3 -16
View File
@@ -237,11 +237,7 @@ pub fn meal_picker(meals: &[Meal], list_id: i64, csrf_token: &str) -> Markup {
} }
} }
pub fn meal_form_page( pub fn meal_form_page(user: &User, meal: Option<&Meal>, csrf_token: &str) -> Markup {
user: &User,
meal: Option<&Meal>,
csrf_token: &str,
) -> Markup {
let (title, action, name, description) = match meal { let (title, action, name, description) = match meal {
Some(meal) => ( Some(meal) => (
"Edit meal", "Edit meal",
@@ -276,12 +272,7 @@ pub fn meal_form_page(
) )
} }
pub fn meal_page( pub fn meal_page(user: &User, meal: &Meal, categories: &[Category], csrf_token: &str) -> Markup {
user: &User,
meal: &Meal,
categories: &[Category],
csrf_token: &str,
) -> Markup {
page( page(
&meal.name, &meal.name,
Some(user), Some(user),
@@ -743,11 +734,7 @@ fn category_options(categories: &[Category], selected: Option<i64>) -> Markup {
} }
} }
pub fn categories_panel( pub fn categories_panel(categories: &[Category], csrf_token: &str, out_of_band: bool) -> Markup {
categories: &[Category],
csrf_token: &str,
out_of_band: bool,
) -> Markup {
let panel = html! { let panel = html! {
div class="panel-heading" { div class="panel-heading" {
h2 { "Categories" } h2 { "Categories" }