Compare commits
3
Commits
4adcfb1377
...
279cdb869d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
279cdb869d | ||
|
|
fbb49bc560 | ||
|
|
7309971c74 |
@@ -6,3 +6,4 @@
|
|||||||
/e2e/node_modules/
|
/e2e/node_modules/
|
||||||
/e2e/test-results/
|
/e2e/test-results/
|
||||||
/e2e/playwright-report/
|
/e2e/playwright-report/
|
||||||
|
/test-results/
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user