From e0d6ed029f949a8bf4d3ea92a25efaac01914789 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 03:53:35 +0000 Subject: [PATCH 001/199] Initial plan From dc6af59cd649b12582776ff0fc403a27ee49b996 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:13:52 +0000 Subject: [PATCH 002/199] Initial plan From 5c11fc8d9a32f083098a9f35d4afa239a2e7ab54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:02:27 +0000 Subject: [PATCH 003/199] Initial plan From 4cf6421eebb31dc72646074bbb3170582d9ad9a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:06:10 +0000 Subject: [PATCH 004/199] Initial plan From 852bd4daae4b3205761b4a7286ad3f25500aff85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:40:40 +0000 Subject: [PATCH 005/199] Initial plan From 79469f70522fd5aad2193cffa75ee84569c156b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:51:18 +0000 Subject: [PATCH 006/199] Initial plan From 66b2096322105240f19fe9214794f48cf8eb804b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:18:54 +0000 Subject: [PATCH 007/199] Initial plan From 7a04f8711729283e1d4f5237922e36d5a86a9a85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:55:48 +0000 Subject: [PATCH 008/199] Initial plan From f31465d547f530b36555cd84b593cf5c57e9a21f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:18:22 +0000 Subject: [PATCH 009/199] Initial plan From bd607fd9417a5d82d5e467be8fa638de4f2c8aa3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 16:36:05 +0000 Subject: [PATCH 010/199] Initial plan From 6afacd7a519bfec7ea9f4983351881a327ceacd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:52:34 +0000 Subject: [PATCH 011/199] Initial plan From bb8892bd257c2992bbf8e1b37d154df9afb9c5b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:00:00 +0000 Subject: [PATCH 012/199] Initial plan From cdd67b7702eaea24eaccf0984f99d7cbd2c9a28f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:02:30 +0000 Subject: [PATCH 013/199] Initial plan From 41de9b155598e59b0598a5b68f304dc8f141416d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:29:26 +0000 Subject: [PATCH 014/199] Initial plan From 48e0faca713656c900101155a95e2192e488c842 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:35:11 +0000 Subject: [PATCH 015/199] Initial plan From af245b26fcbafc5d4641088f1668ebe4b830ed84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:58:16 +0000 Subject: [PATCH 016/199] Initial plan From e000a2d653b19a9272c94a1fe6e8006ce63f96ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:02:14 +0000 Subject: [PATCH 017/199] Initial plan From 731e12e7b0916e5e510c56a3869514f219a63ffe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:13:26 +0000 Subject: [PATCH 018/199] Initial plan From 948d66aaa9040a7d40d9ba4826a3dfbeb32ef94a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:52:05 +0000 Subject: [PATCH 019/199] Initial plan From 322c666ee86f1874238a23d50df1d40e23e007d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:27:09 +0000 Subject: [PATCH 020/199] Initial plan From 411f9bb47339fa0eec542c03444a44a576d2f6e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:28:23 +0000 Subject: [PATCH 021/199] Initial plan From ff91046e2c6c3aa1a91b44c4e746729e45a9f107 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 12:08:10 +0000 Subject: [PATCH 022/199] Initial plan From 52a6d0583706b3fb40bec2dac5ef422fee742780 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:01:11 +0000 Subject: [PATCH 023/199] Initial plan From 5191d1c544cc8c26b92459d261cddc73a9c90680 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 08:30:18 +0000 Subject: [PATCH 024/199] Initial plan From bb310ad967efd831b1cfec664c9269dd47f85b15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:51:23 +0000 Subject: [PATCH 025/199] Initial plan From 90dee1e849398477b40e4a47037cb66c25ef9b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:18:26 +0000 Subject: [PATCH 026/199] Initial plan From 10052e96f2ff897b3e1a382f5fb7577b5f390835 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:11:09 +0000 Subject: [PATCH 027/199] Initial plan From 868d45b33dac5305ce41d3297eddfe5698fa139f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:55:03 +0000 Subject: [PATCH 028/199] Initial plan From ab4e9089394678a35b6625e5b7c4732db60d9515 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 10:23:19 +0000 Subject: [PATCH 029/199] Initial plan From 2179f535b9913ebdf025b9ccf3e682e02fe428a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:00:34 +0000 Subject: [PATCH 030/199] Initial plan From 8da2d77d37bb6406a3069856712fb9be0d0c017b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:28:49 +0000 Subject: [PATCH 031/199] Initial plan From 8e9055a5db79357d02522b340f37e1d4d931f99b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:22:36 +0000 Subject: [PATCH 032/199] Initial plan From de81a6a56350e7db3bdb15a3a658c4b49c969824 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:01:50 +0000 Subject: [PATCH 033/199] Initial plan From 8d266a5d33232ad82a5244e320fd1a22b3691666 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:21:03 +0000 Subject: [PATCH 034/199] Initial plan From 82f02118a5e9a27baeb54b13c5a1edf81db8f35a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:19:05 +0000 Subject: [PATCH 035/199] Initial plan From 1e5d24a69c7fdc21395849494ea7f37895c91bb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:54:02 +0000 Subject: [PATCH 036/199] Initial plan From a32109dee6f72d5bb226aec74ebfd3e534469d91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 04:44:42 +0000 Subject: [PATCH 037/199] Initial plan From 65b7dcef428ed1f27d7c16abec15ae8ae5991a0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:25:54 +0000 Subject: [PATCH 038/199] Initial plan From 466a63984dcc657c90ff2fcc6467276da70c6003 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:14:06 +0000 Subject: [PATCH 039/199] Initial plan From 7529e0ac8d21c2f82ed78ca2ff73ae2883a68408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:26:24 +0000 Subject: [PATCH 040/199] Initial plan From 7f946d59f6148554a47111c8b15b6cd007b8ec52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:12:33 +0000 Subject: [PATCH 041/199] Initial plan From 869c92a0600d4237279a22ae9e59998986f7230d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:17:26 +0000 Subject: [PATCH 042/199] Initial plan From eb74652422947ccd12c6b95253d05a51b0145113 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:30:05 +0000 Subject: [PATCH 043/199] Initial plan From 7b361d4365e1e06433b369ddc72dc73c7b77a43a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:25:27 +0000 Subject: [PATCH 044/199] Initial plan From 371f05242f0eb8aeb95ed13692b5793c635fec61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:29:21 +0000 Subject: [PATCH 045/199] Initial plan From cab38cd9b7600f8e2ad165819a67d28623164dea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:20:43 +0000 Subject: [PATCH 046/199] Initial plan From f6b0590de6772735b5dc6294bd665be309752d7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:24:47 +0000 Subject: [PATCH 047/199] Initial plan From 44b81213050d9f2de4b32d3aa10d84b4bda262a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:20:53 +0000 Subject: [PATCH 048/199] Initial plan From 19701590e29528c630e8d47cca2a56537a81afad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:01:39 +0000 Subject: [PATCH 049/199] Initial plan From 36ac0ed90739f88a4c137e755f3691b295c42b63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:59:36 +0000 Subject: [PATCH 050/199] Initial plan From ee6c11eb2f4e2bc8b80432bae4092c986f637f98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:22:12 +0000 Subject: [PATCH 051/199] Initial plan From 093bd5ebe7decabba93cb7eee4b4e3bd88790b39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:07 +0000 Subject: [PATCH 052/199] Initial plan From 56cb5f1ba10225bcf9999b12aaa79d200b13d4d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:37:32 +0000 Subject: [PATCH 053/199] Initial plan From f259bfeb622d0c27e1d39f3e2590dcac6fc46c01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:04 +0000 Subject: [PATCH 054/199] Initial plan From e1392cb9f6355de173d90e87cce9439122faae4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 04:31:17 +0000 Subject: [PATCH 055/199] Initial plan From f8f371fd66a7141f071c5831d2ea590b931ec5d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:17 +0000 Subject: [PATCH 056/199] Initial plan From 8c44672cff60289f979a99e9e3a5e544cd1d04bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:32:10 +0000 Subject: [PATCH 057/199] Initial plan From c7e92bf268466373210f83e78531472670d5404c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 06:54:04 +0000 Subject: [PATCH 058/199] Initial plan From 15b8eaa85cacdedc02a25eef5de5578e8c432988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 04:30:34 +0000 Subject: [PATCH 059/199] Initial plan From a3bc1be04cf1c8c6e26200c803ff62d5673c3206 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:48:46 +0000 Subject: [PATCH 060/199] Initial plan From aa27073c6a6a6c576b0dedc58c5f38ccf2e253f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 07:55:29 +0000 Subject: [PATCH 061/199] Initial plan From 5b534a5926f268d6579c793e710764081305b12d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:29:52 +0000 Subject: [PATCH 062/199] Initial plan From a6bbfb0c1a90fca4f924b03de70b549cedc18e9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:31:15 +0000 Subject: [PATCH 063/199] Initial plan From 73750ce5545447de21c40e5f9f314d0e2ffebd6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:52:10 +0000 Subject: [PATCH 064/199] Initial plan From a7850037423e0e6f71bf5faa7df1997c6db153d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:08 +0000 Subject: [PATCH 065/199] Initial plan From f65c5747607b5748b9be16614c889efab781c232 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:05:50 +0000 Subject: [PATCH 066/199] Initial plan From 88a703faa5a581dbb0dce2bc1ae3a854e8dae28b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:00:29 +0000 Subject: [PATCH 067/199] Initial plan From 97aad3df32aff1746a9d372184a63d133d7d24cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:22:12 +0000 Subject: [PATCH 068/199] Initial plan From 7732bb73adf8cd67c90521b7db5c8a537b830803 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:24:28 +0000 Subject: [PATCH 069/199] Initial plan From 986ee97643fdb05ee75a19b17aa8e990a9203fee Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Fri, 13 Mar 2026 11:12:15 +0100 Subject: [PATCH 070/199] fix(expo/CreatePackItemForm): default quantity to 1 instead of 0 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> From cc775c7656942aad1e636f95aba65c6814b7de47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:47:34 +0000 Subject: [PATCH 071/199] Initial plan From 80e2b8235a659a017256945543df5b25931f7d49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:14:42 +0000 Subject: [PATCH 072/199] Initial plan From 0296b3935292d8714eae139d0dfe1f5c9cb4b302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:02 +0000 Subject: [PATCH 073/199] Initial plan From fab0b0a71d0aae86ad5f83ca84e327acf9b3739e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:00:55 +0000 Subject: [PATCH 074/199] Initial plan From 649f245d49e8e1aba957fa56d3497ee4be7cad84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:30:58 +0000 Subject: [PATCH 075/199] Initial plan From cfeff1e9f3f0a724be5f2627b9530ef790bc0e72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:19 +0000 Subject: [PATCH 076/199] Initial plan From 985bac85fb9bcb35b04c211440a3cecaaac30c92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:10 +0000 Subject: [PATCH 077/199] Initial plan From aa2ee754ceb782239ef5379aceec41151a245932 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:02:55 +0000 Subject: [PATCH 078/199] Initial plan From 7de6a588c03c7ce3b048abad623142d81e3a4bd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:01:29 +0000 Subject: [PATCH 079/199] Initial plan From d4559797ae9ef2d9f57f8da75542fe1027917885 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:06 +0000 Subject: [PATCH 080/199] Initial plan From 701414931e7ef945d1222cf2a3b46509e6888a0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:18 +0000 Subject: [PATCH 081/199] Initial plan From 2567058dc7d86783a0163759ea03332c6b1f97b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:00:23 +0000 Subject: [PATCH 082/199] Initial plan From 1b0f65797302f505e2820cf25cf5ad08c8b8f06d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:08:55 +0000 Subject: [PATCH 083/199] Initial plan From 7d1bc13dafd665254a65776fede16169bcedf611 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:09 +0000 Subject: [PATCH 084/199] Initial plan From 5bf077f7c7e5875286e80ccd938dd4b238587d4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:03 +0000 Subject: [PATCH 085/199] Initial plan From 03e1d84d38fdd378f6629dff5a385ddfff53b064 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:25:55 +0000 Subject: [PATCH 086/199] Initial plan From 7cee6f96b219f31620de8ee6b9851818f85c879b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:01 +0000 Subject: [PATCH 087/199] Initial plan From 3938af120ac3e7a04be2a13dc8b888bb9a628b02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:03 +0000 Subject: [PATCH 088/199] Initial plan From bb4ce473ff38dad2098588da987436e6827302f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:25:56 +0000 Subject: [PATCH 089/199] Initial plan From d3448456174e2232a881b93b67da235107c04b14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:55:45 +0000 Subject: [PATCH 090/199] Initial plan From 49eacf7ff4a1bf2d5d78b597d6f61ec8381df425 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 04:34:47 +0000 Subject: [PATCH 091/199] Initial plan From 31d4de15a186ac9e2bdb5cfe23f85ce79086be6e Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Sat, 11 Apr 2026 07:31:04 -0600 Subject: [PATCH 092/199] chore: reopen trigger (no-op commit to restore PR state) From 9512302c4f610532dd31e7eaf9fe5fa25902a9d0 Mon Sep 17 00:00:00 2001 From: Andrew Bierman <94939237+andrew-bierman@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:37:38 -0600 Subject: [PATCH 093/199] ci: trigger biome check From 8f7c7e1d534786e77493de6c8f550570841e3c20 Mon Sep 17 00:00:00 2001 From: Andrew Bierman <94939237+andrew-bierman@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:48:54 -0600 Subject: [PATCH 094/199] ci: retrigger CI after biome fixes From 5b57c021b97f0ae2668309156acf3bfc06f62bb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 05:12:33 +0000 Subject: [PATCH 095/199] Initial plan From 061042293abd1a1e0a8d9c4449c2e353bdc30a9a Mon Sep 17 00:00:00 2001 From: Andrew Bierman <94939237+andrew-bierman@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:30:28 -0600 Subject: [PATCH 096/199] ci: trigger checks for dependabot merges From 53ecc0d6fe1c8012583f01b4f23f7a06143d1af7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:04:18 +0000 Subject: [PATCH 097/199] Initial plan From 7251017326e53531a81ca180fd837161c4f2e160 Mon Sep 17 00:00:00 2001 From: Andrew Bierman <94939237+andrew-bierman@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:22:54 -0600 Subject: [PATCH 098/199] ci: retrigger checks after copilot merge-conflict fix From c70bc562f20e5bfe862c7ea6b9aaebc341239802 Mon Sep 17 00:00:00 2001 From: Andrew Bierman <94939237+andrew-bierman@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:56:34 -0600 Subject: [PATCH 099/199] ci: trigger CI on Copilot bot's expo-symbols type fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-trigger CI after bot commit 205bc1f2 (renderingMode→type, SfSymbolName types) which was blocked at action_required. From 1eab201e5855fe65e75bc0124de7e9973f5a6826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:14:52 +0000 Subject: [PATCH 100/199] Initial plan From 8dcc228f083407d611040739dc1ef62961a5f21f Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 14 Apr 2026 18:07:09 -0600 Subject: [PATCH 101/199] trigger: retrigger CI after node_modules clean install verified vitest-pool-workers 0.14.6 resolves correctly From aca97fc4f8f855d1425b915c079c0a6c499af85b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:16:32 +0000 Subject: [PATCH 102/199] Initial plan From 2980aa0213a86aad4a0aa58c60feec59b9922e60 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 14 Apr 2026 18:07:09 -0600 Subject: [PATCH 103/199] trigger: retrigger CI after node_modules clean install verified vitest-pool-workers 0.14.6 resolves correctly From c98fd6c255e5e72c91c17b1a65d7f5f23fed663d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 12:03:40 +0000 Subject: [PATCH 104/199] ci: re-trigger checks after @types/react alignment fix https://claude.ai/code/session_01UPTmzL2JtXyTZSpbyqE9j5 From 7bb6abbb5a97118bf1a748beedfd3014f9b0ccf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:16:32 +0000 Subject: [PATCH 105/199] Initial plan From 3bcb8d7e1d334253dc442001a7f9993a8bb940cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:54:40 +0000 Subject: [PATCH 106/199] Initial plan From 4a78d26ec518255e29cfcfac723e66d7ee39a315 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 05:15:58 +0000 Subject: [PATCH 107/199] ci: trigger CI run on updated branch https://claude.ai/code/session_017iNQZKZ9tbC8AfJ6qL1aqu From 21d9e9350e20325bced5fc9ee9740ce02f61f893 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 07:32:59 +0000 Subject: [PATCH 108/199] Initial plan From a16b922829f5560732130a0c112bc13058f6c9f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 07:32:59 +0000 Subject: [PATCH 109/199] Initial plan From 9105ec93c32b405d386e2b2d5f06b42043528e40 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 14 Apr 2026 07:45:06 -0600 Subject: [PATCH 110/199] ci: trigger workflows From bbb46aef75ceb580292d0551b9081657e41200d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 06:54:22 +0000 Subject: [PATCH 111/199] ci: retrigger CI after suspected transient runner failure https://claude.ai/code/session_01LJnh37hSqTY8VMsqNyVRSb From 894a0b67dafd69503589106c43ac560e51080015 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Wed, 29 Apr 2026 06:59:35 -0600 Subject: [PATCH 112/199] ci: retrigger workflows From 97771d5a4104c05c3e4dc65868faeeb93c288309 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Fri, 1 May 2026 00:10:26 -0600 Subject: [PATCH 113/199] =?UTF-8?q?feat(web):=20web=20support=20MVP=20?= =?UTF-8?q?=E2=80=94=20platform=20shims,=20Playwright=20e2e,=20OTP=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform shims (lib/*.ts + lib/*.web.ts): - lib/Picker: web onValueChange?.(e.target.value)} + style={{ + display: 'block', + width: '100%', + padding: '8px', + borderRadius: '6px', + border: '1px solid #ccc', + fontSize: '16px', + }} + > + {options.map((opt) => ( + + ))} + + ); +} + +Picker.Item = PickerItem; + +export { Picker }; diff --git a/apps/expo/lib/appleAuthentication.ts b/apps/expo/lib/appleAuthentication.ts new file mode 100644 index 0000000000..ca7ce9cfa4 --- /dev/null +++ b/apps/expo/lib/appleAuthentication.ts @@ -0,0 +1,7 @@ +export { + AppleAuthenticationOperation, + AppleAuthenticationScope, + AppleAuthenticationUserDetectionStatus, + isAvailableAsync, + signInAsync, +} from 'expo-apple-authentication'; diff --git a/apps/expo/lib/appleAuthentication.web.ts b/apps/expo/lib/appleAuthentication.web.ts new file mode 100644 index 0000000000..ec609349f0 --- /dev/null +++ b/apps/expo/lib/appleAuthentication.web.ts @@ -0,0 +1,12 @@ +export const isAvailableAsync = (): Promise => Promise.resolve(false); + +export const signInAsync = (): Promise => + Promise.reject(new Error('Apple Sign-In is not available on web.')); + +export const AppleAuthenticationScope = { FULL_NAME: 0, EMAIL: 1 }; +export const AppleAuthenticationOperation = { LOGIN: 0, REFRESH: 1, LOGOUT: 2, IMPLICIT: 3 }; +export const AppleAuthenticationUserDetectionStatus = { + UNKNOWN: 0, + UNSUPPORTED: 1, + LIKELY_REAL: 2, +}; diff --git a/apps/expo/lib/constants.web.ts b/apps/expo/lib/constants.web.ts new file mode 100644 index 0000000000..5be19f6d0c --- /dev/null +++ b/apps/expo/lib/constants.web.ts @@ -0,0 +1,5 @@ +/** + * Web equivalent of lib/constants.ts. + * There is no filesystem-backed image cache on web; the browser handles caching. + */ +export const IMAGES_DIR = ''; diff --git a/apps/expo/lib/devClient.ts b/apps/expo/lib/devClient.ts new file mode 100644 index 0000000000..15703db6e3 --- /dev/null +++ b/apps/expo/lib/devClient.ts @@ -0,0 +1 @@ +import 'expo-dev-client'; diff --git a/apps/expo/lib/devClient.web.ts b/apps/expo/lib/devClient.web.ts new file mode 100644 index 0000000000..5d7e59fc5c --- /dev/null +++ b/apps/expo/lib/devClient.web.ts @@ -0,0 +1 @@ +// expo-dev-client is not needed on web diff --git a/apps/expo/lib/hooks/useColorScheme.web.tsx b/apps/expo/lib/hooks/useColorScheme.web.tsx new file mode 100644 index 0000000000..971bb9d2c1 --- /dev/null +++ b/apps/expo/lib/hooks/useColorScheme.web.tsx @@ -0,0 +1,37 @@ +import { COLORS } from 'expo-app/theme/colors'; +import { useColorScheme as useNativewindColorScheme } from 'nativewind'; +import * as React from 'react'; + +/** + * Web version of useColorScheme. + * Removes the expo-navigation-bar dependency (Android-only native module). + * Metro automatically picks this file over useColorScheme.tsx for web builds. + */ +function useColorScheme() { + const { colorScheme, setColorScheme: setNativeWindColorScheme } = useNativewindColorScheme(); + + function setColorScheme(scheme: 'light' | 'dark') { + setNativeWindColorScheme(scheme); + } + + function toggleColorScheme() { + return setColorScheme(colorScheme === 'light' ? 'dark' : 'light'); + } + + return { + colorScheme: colorScheme ?? 'light', + isDarkColorScheme: colorScheme === 'dark', + setColorScheme, + toggleColorScheme, + colors: COLORS[colorScheme ?? 'light'], + }; +} + +/** + * No-op on web — Android navigation bar sync is not needed in the browser. + */ +function useInitialAndroidBarSync() { + React.useEffect(() => {}, []); +} + +export { useColorScheme, useInitialAndroidBarSync }; diff --git a/apps/expo/lib/updates.ts b/apps/expo/lib/updates.ts new file mode 100644 index 0000000000..f613d0b7eb --- /dev/null +++ b/apps/expo/lib/updates.ts @@ -0,0 +1,10 @@ +export { + channel, + checkForUpdateAsync, + fetchUpdateAsync, + isEnabled, + reloadAsync, + runtimeVersion, + updateId, + useUpdates, +} from 'expo-updates'; diff --git a/apps/expo/lib/updates.web.ts b/apps/expo/lib/updates.web.ts new file mode 100644 index 0000000000..073468ae4a --- /dev/null +++ b/apps/expo/lib/updates.web.ts @@ -0,0 +1,11 @@ +export const reloadAsync = async () => { + window.location.reload(); +}; + +export const checkForUpdateAsync = async () => ({ isAvailable: false }); +export const fetchUpdateAsync = async () => ({ isNew: false }); +export const useUpdates = () => ({ isUpdateAvailable: false, isUpdatePending: false }); +export const isEnabled = false; +export const channel = 'web'; +export const updateId = null; +export const runtimeVersion = '0.0.0'; diff --git a/apps/expo/lib/utils/ImageCacheManager.web.ts b/apps/expo/lib/utils/ImageCacheManager.web.ts new file mode 100644 index 0000000000..3e71cdcaf9 --- /dev/null +++ b/apps/expo/lib/utils/ImageCacheManager.web.ts @@ -0,0 +1,31 @@ +/** + * Web stub for ImageCacheManager. + * The browser handles HTTP caching natively; no local file cache is needed on web. + * All methods are safe no-ops so that callers compile and run without changes. + */ +class WebImageCacheManager { + public cacheDirectory = ''; + + public async initCacheDirectory(): Promise {} + + public async getCachedImageUri(_fileName: string): Promise { + return null; + } + + public async cacheRemoteImage(_fileName: string, remoteUrl: string): Promise { + return remoteUrl; + } + + public async cacheLocalTempImage(_tempImageUri: string, _fileName: string): Promise {} + + public async clearImage(_fileName: string): Promise {} + + public async clearCache(): Promise {} + + public async getCacheInfo(): Promise<{ size: number; count: number }> { + return { size: 0, count: 0 }; + } +} + +export { WebImageCacheManager as ImageCacheManager }; +export default new WebImageCacheManager(); diff --git a/apps/expo/lib/utils/getRelativeTime.ts b/apps/expo/lib/utils/getRelativeTime.ts index d77644a356..d1dd074b5e 100644 --- a/apps/expo/lib/utils/getRelativeTime.ts +++ b/apps/expo/lib/utils/getRelativeTime.ts @@ -7,7 +7,7 @@ const UNITS: Array<{ key: string; seconds: number }> = [ { key: 'days', seconds: 86400 }, { key: 'hours', seconds: 3600 }, { key: 'minutes', seconds: 60 }, -]; +] as const; function toDate(value: Date | string | null | undefined): Date | null { if (!value) return null; diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index 2613f966d0..a9932c2b58 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -24,12 +24,13 @@ const WEB_STUBS = { '@react-native-ai/llama': 'mocks/react-native-ai-llama.ts', 'llama.rn': 'mocks/react-native-ai-llama.ts', '@react-native-ai/apple': 'mocks/react-native-ai-apple.ts', + '@react-native-google-signin/google-signin': 'mocks/react-native-google-signin.ts', 'expo-sqlite/kv-store': 'mocks/expo-sqlite-kv-store.ts', + // Required by lib/persist-plugin.web.ts (ObservablePersistAsyncStorage) + '@react-native-async-storage/async-storage': 'mocks/async-storage.ts', // Keyboard utilities — on web the software keyboard doesn't overlay content 'react-native-keyboard-controller': 'mocks/react-native-keyboard-controller.tsx', - // Google Sign-In and date picker are native-only; web uses password auth - '@react-native-google-signin/google-signin': 'mocks/google-signin.ts', - '@react-native-community/datetimepicker': 'mocks/datetimepicker.tsx', + '@react-native-community/datetimepicker': 'mocks/react-native-community-datetimepicker.tsx', // expo-file-system throws UnavailabilityError on web; stub all ops as no-ops 'expo-file-system/legacy': 'mocks/expo-file-system-legacy.ts', }; diff --git a/apps/expo/mocks/expo-sqlite-kv-store.ts b/apps/expo/mocks/expo-sqlite-kv-store.ts index ea7305a655..0ecd3639e1 100644 --- a/apps/expo/mocks/expo-sqlite-kv-store.ts +++ b/apps/expo/mocks/expo-sqlite-kv-store.ts @@ -2,31 +2,27 @@ import { isFunction } from '@packrat/guards'; type UpdateFn = (prevValue: string | null) => string; -const PREFIX = '__kv__'; - const isClient = typeof window !== 'undefined'; const memFallback = new Map(); const rawGet = (key: string): string | null => - isClient ? window.localStorage.getItem(PREFIX + key) : (memFallback.get(key) ?? null); + isClient ? window.localStorage.getItem(key) : (memFallback.get(key) ?? null); const rawSet = (key: string, value: string): void => { - if (isClient) window.localStorage.setItem(PREFIX + key, value); + if (isClient) window.localStorage.setItem(key, value); else memFallback.set(key, value); }; const rawRemove = (key: string): boolean => { const had = rawGet(key) !== null; - if (isClient) window.localStorage.removeItem(PREFIX + key); + if (isClient) window.localStorage.removeItem(key); else memFallback.delete(key); return had; }; const rawKeys = (): string[] => { if (!isClient) return Array.from(memFallback.keys()); - return Object.keys(window.localStorage) - .filter((k) => k.startsWith(PREFIX)) - .map((k) => k.slice(PREFIX.length)); + return Object.keys(window.localStorage); }; const deepMerge = ( diff --git a/apps/expo/mocks/react-native-community-datetimepicker.tsx b/apps/expo/mocks/react-native-community-datetimepicker.tsx new file mode 100644 index 0000000000..a437cced6a --- /dev/null +++ b/apps/expo/mocks/react-native-community-datetimepicker.tsx @@ -0,0 +1,57 @@ +import type * as React from 'react'; + +type DateTimePickerEvent = { type: string; nativeEvent: { timestamp: number } }; + +type Props = { + value: Date; + mode?: 'date' | 'time' | 'datetime'; + onChange?: (event: DateTimePickerEvent, date?: Date) => void; + display?: string; + minimumDate?: Date; + maximumDate?: Date; + style?: unknown; +}; + +function toInputValue(date: Date, mode: Props['mode']): string { + if (mode === 'time') return date.toTimeString().slice(0, 5); + if (mode === 'datetime') + return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16); + return date.toISOString().split('T')[0] ?? ''; +} + +export default function DateTimePicker({ + value, + mode = 'date', + onChange, + minimumDate, + maximumDate, +}: Props) { + const inputType = mode === 'time' ? 'time' : mode === 'datetime' ? 'datetime-local' : 'date'; + + function handleChange(e: React.ChangeEvent) { + if (!onChange) return; + const raw = e.target.value; + if (!raw) return; + const date = new Date(mode === 'time' ? `1970-01-01T${raw}` : raw); + onChange({ type: 'set', nativeEvent: { timestamp: date.getTime() } }, date); + } + + return ( + + ); +} diff --git a/apps/expo/mocks/react-native-google-signin.ts b/apps/expo/mocks/react-native-google-signin.ts new file mode 100644 index 0000000000..8e684c27da --- /dev/null +++ b/apps/expo/mocks/react-native-google-signin.ts @@ -0,0 +1,20 @@ +// Web stub: Google Sign-In is a native-only SDK. On web, sign-in throws immediately. +export const GoogleSignin = { + hasPlayServices: (): Promise => Promise.resolve(true), + signIn: (): Promise => + Promise.reject(new Error('Google Sign-In is not supported on web. Please use email/password.')), + getTokens: (): Promise<{ idToken: string | null; accessToken: string | null }> => + Promise.resolve({ idToken: null, accessToken: null }), + hasPreviousSignIn: (): Promise => Promise.resolve(false), + signOut: (): Promise => Promise.resolve(), + configure: (): void => {}, +}; + +export const isErrorWithCode = (_error: unknown): boolean => false; + +export const statusCodes = { + SIGN_IN_CANCELLED: 'SIGN_IN_CANCELLED', + IN_PROGRESS: 'IN_PROGRESS', + PLAY_SERVICES_NOT_AVAILABLE: 'PLAY_SERVICES_NOT_AVAILABLE', + SIGN_IN_REQUIRED: 'SIGN_IN_REQUIRED', +}; diff --git a/apps/expo/package.json b/apps/expo/package.json index a2f8a08bbc..76094d2001 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -35,6 +35,8 @@ "submit:ios": "eas submit --platform ios", "test": "vitest run", "test:coverage": "vitest run --coverage", + "test:web": "playwright test --config playwright/playwright.config.ts", + "test:web:ui": "playwright test --config playwright/playwright.config.ts --ui", "update:development": "APP_VARIANT=development eas update --branch development --environment development", "update:preview": "APP_VARIANT=preview eas update --branch preview --environment preview", "update:production": "eas update --branch production --environment production", diff --git a/apps/expo/playwright/playwright.config.ts b/apps/expo/playwright/playwright.config.ts new file mode 100644 index 0000000000..30049dc98e --- /dev/null +++ b/apps/expo/playwright/playwright.config.ts @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test'; + +const BASE_URL = process.env.BASE_URL ?? 'http://localhost:8081'; + +export default defineConfig({ + testDir: './tests', + globalSetup: './tests/globalSetup.ts', + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [['list'], ['html', { open: 'never' }]], + + use: { + baseURL: BASE_URL, + trace: 'on-first-retry', + video: 'on-first-retry', + headless: true, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/apps/expo/playwright/tests/core.spec.ts b/apps/expo/playwright/tests/core.spec.ts new file mode 100644 index 0000000000..a458f48774 --- /dev/null +++ b/apps/expo/playwright/tests/core.spec.ts @@ -0,0 +1,267 @@ +/** + * Web E2E tests for PackRat core functionality. + * + * Each test navigates to a route after seeding auth tokens in localStorage. + * TestIds match the constants in lib/testIds.ts and the Maestro iOS flows. + */ +import { BASE_URL, expect, test } from './fixtures'; + +// ─── Dashboard ────────────────────────────────────────────────────────────── + +test('dashboard loads authenticated', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/`); + // Tab bar must be visible — confirms app rendered past the auth gate + await expect(page.getByRole('tab', { name: /Dashboard/i })).toBeVisible(); + await expect(page.getByRole('tab', { name: /Packs/i })).toBeVisible(); +}); + +// ─── Packs ─────────────────────────────────────────────────────────────────── + +test('packs tab loads and shows create button', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/packs`); + await expect(page.getByTestId('create-pack-button')).toBeVisible(); +}); + +test('create a pack end-to-end', async ({ authedPage: page }) => { + const packName = `E2E-Pack-${Date.now()}`; + + // Use waitForResponse to capture the created pack ID. + // Navigating directly to /pack/new means router.back() fails on submit, + // so we intercept the API response instead of relying on navigation. + const [packResponse] = await Promise.all([ + page.waitForResponse((r) => r.url().includes('/api/packs') && r.request().method() === 'POST'), + (async () => { + await page.goto(`${BASE_URL}/pack/new`); + await page.getByRole('textbox', { name: /Pack Name/i }).fill(packName); + await page.getByTestId('submit-pack-button').click(); + })(), + ]); + + expect(packResponse.ok()).toBeTruthy(); + + // Verify pack appears in the list + await page.goto(`${BASE_URL}/packs`); + await expect(page.getByText(packName)).toBeVisible({ timeout: 10_000 }); +}); + +// ─── Pack Detail — add items ───────────────────────────────────────────────── + +test('add item manually to a pack', async ({ authedPage: page }) => { + const packName = `E2E-AddItem-${Date.now()}`; + + // Create a pack via API and capture the ID + const [packResponse] = await Promise.all([ + page.waitForResponse((r) => r.url().includes('/api/packs') && r.request().method() === 'POST'), + (async () => { + await page.goto(`${BASE_URL}/pack/new`); + await page.getByTestId('packs:name-input').fill(packName); + await page.getByTestId('submit-pack-button').click(); + })(), + ]); + + expect(packResponse.ok()).toBeTruthy(); + const { id: packId } = (await packResponse.json()) as { id: number }; + + // Fill the item creation form using testIds + await page.goto(`${BASE_URL}/item/new?packId=${packId}`); + await page.getByTestId('items:name-input').fill('Test Tent'); + await page.getByTestId('items:weight-input').fill('1200'); + + // Register listener BEFORE clicking — syncedCrud initiates the POST shortly after form submit. + // We must await the response BEFORE page.goto() because a full navigation aborts in-flight requests. + const itemPostPromise = page.waitForResponse( + (r) => + r.url().includes('/api/packs') && + r.url().includes('/items') && + r.request().method() === 'POST', + { timeout: 15_000 }, + ); + + await page.getByTestId('items:submit').click(); + + // Wait for the item to land in the DB before navigating away + await itemPostPromise; + + // Now safe: item is persisted, page.goto() won't abort anything critical + await page.goto(`${BASE_URL}/pack/${packId}`); + await expect(page.getByText('Test Tent')).toBeVisible({ timeout: 15_000 }); +}); + +test('add item from catalog to a pack', async ({ authedPage: page }) => { + const packName = `E2E-Catalog-${Date.now()}`; + + // Create a pack and capture the ID + const [packResponse] = await Promise.all([ + page.waitForResponse((r) => r.url().includes('/api/packs') && r.request().method() === 'POST'), + (async () => { + await page.goto(`${BASE_URL}/pack/new`); + await page.getByRole('textbox', { name: /Pack Name/i }).fill(packName); + await page.getByTestId('submit-pack-button').click(); + })(), + ]); + + const { id: packId } = (await packResponse.json()) as { id: number }; + + // Navigate to pack detail and open "Add from Catalog" sheet + await page.goto(`${BASE_URL}/pack/${packId}`); + await page.getByTestId('add-from-catalog-option').last().click(); + + // Dialog with catalog items should appear + await expect(page.getByText('Browse Catalog').first()).toBeVisible({ timeout: 10_000 }); + + // Wait for catalog items to load, then click the first one + const firstCard = page.getByTestId(/^catalog-item-card-/).first(); + await firstCard.waitFor({ timeout: 15_000 }); + await firstCard.click(); + + // Confirm "Add N item(s)" panel appears and click it + await expect(page.getByText(/Add \d+ item/i)).toBeVisible({ timeout: 5_000 }); + await page.getByText(/Add \d+ item/i).click(); + + // Local store updates synchronously; the pack detail (behind the modal) re-renders. + // A non-zero weight confirms the catalog item was added. + await expect(page.getByText(/[1-9]\d*\.?\d*g/).first()).toBeVisible({ timeout: 10_000 }); +}); + +// ─── Trips ──────────────────────────────────────────────────────────────────── + +test('trips tab loads', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/trips`); + await expect(page.getByText('Create New Trip')).toBeVisible(); +}); + +test('create a trip with dates', async ({ authedPage: page }) => { + test.setTimeout(60_000); + const tripName = `E2E-Trip-${Date.now()}`; + + const postPromise = page.waitForResponse( + (r) => r.url().includes('/api/trips') && r.request().method() === 'POST', + { timeout: 20_000 }, + ); + + await page.goto(`${BASE_URL}/trip/new`); + const nameInput = page.getByTestId('trips:name-input'); + await nameInput.waitFor({ timeout: 10_000 }); + await nameInput.fill(tripName); + + // Open start date picker and set via native input + await page + .getByText(/Start Date/i) + .first() + .click(); + const startInput = page.locator('input[type="date"]').first(); + await startInput.waitFor({ timeout: 5_000 }); + await startInput.fill('2026-08-01'); + + // Open end date picker + await page + .getByText(/End Date/i) + .first() + .click(); + const endInput = page.locator('input[type="date"]').last(); + await endInput.waitFor({ timeout: 5_000 }); + await endInput.fill('2026-08-14'); + + await page.getByTestId('submit-trip-button').click(); + + // Wait for the POST to complete so the trip is persisted before navigating + const response = await postPromise; + expect(response.ok()).toBeTruthy(); + + // Navigate to trips list and verify + await page.goto(`${BASE_URL}/trips`); + await page.waitForLoadState('networkidle'); + await expect(page.getByText(tripName)).toBeVisible({ timeout: 15_000 }); +}); + +// ─── Catalog ────────────────────────────────────────────────────────────────── + +test('catalog tab loads items', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/catalog`); + // Wait for items to load — at least one item name visible + await expect(page.locator('text=/\\d+,?\\d+ items/i').first()).toBeVisible({ timeout: 15_000 }); +}); + +test('catalog search filters results', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/catalog`); + // Wait for initial load + await page.waitForLoadState('networkidle'); + + // The search box is revealed by clicking the search icon + await page.getByText('󰍉').first().click(); + + const searchBox = page.locator('input[placeholder*="Search"]'); + await searchBox.waitFor({ timeout: 5_000 }); + await searchBox.fill('sleeping bag'); + // Results should update — check item names + await expect(page.getByText(/sleeping bag/i).first()).toBeVisible({ timeout: 10_000 }); +}); + +// ─── Profile ────────────────────────────────────────────────────────────────── + +test('profile screen loads user info', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/profile`); + await expect(page.getByText('Account Information')).toBeVisible(); + // User email should be visible + await expect(page.getByText(/@/).first()).toBeVisible(); +}); + +test('profile name edit screen', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/profile/name`); + await expect(page.getByRole('heading', { name: 'Name' })).toBeVisible(); + await expect(page.getByRole('textbox')).toHaveCount(2); // First + Last +}); + +// ─── Settings ───────────────────────────────────────────────────────────────── + +test('settings screen loads', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/settings`); + await expect(page.getByText('AI Models')).toBeVisible(); + await expect(page.getByText('Danger Zone')).toBeVisible(); + await expect(page.getByText(/PackRat v/i)).toBeVisible(); +}); + +// ─── AI Chat ────────────────────────────────────────────────────────────────── + +test('AI chat sends message and gets response', async ({ authedPage: page }) => { + test.setTimeout(60_000); // AI streaming responses can take 20-30s + // Create a pack to chat about first + const packName = `E2E-AI-${Date.now()}`; + + const [packResponse] = await Promise.all([ + page.waitForResponse((r) => r.url().includes('/api/packs') && r.request().method() === 'POST'), + (async () => { + await page.goto(`${BASE_URL}/pack/new`); + await page.getByRole('textbox', { name: /Pack Name/i }).fill(packName); + await page.getByTestId('submit-pack-button').click(); + })(), + ]); + + const { id: packId } = (await packResponse.json()) as { id: number }; + + await page.goto( + `${BASE_URL}/ai-chat?packId=${packId}&packName=${encodeURIComponent(packName)}&contextType=pack`, + ); + + // Greet message should be visible + await expect(page.getByText(/working with your/i).first()).toBeVisible(); + + // Send a message + await page.getByRole('textbox', { name: /Ask about this pack/i }).fill('List 3 essential items.'); + // Send button is icon-only with no accessible name; use the arrow-up icon character + await page.getByText('󰁝').click(); + + // Wait for AI response (streaming may take a while) + await expect(page.getByText(/item/i).nth(1)).toBeVisible({ timeout: 30_000 }); +}); + +// ─── Weather ────────────────────────────────────────────────────────────────── + +test('weather screen loads', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/weather`); + await expect(page.getByText('Weather', { exact: true }).first()).toBeVisible(); + // Empty state or locations list + await expect(page.getByText('No saved locations').or(page.locator('text=/°[FC]/'))).toBeVisible({ + timeout: 10_000, + }); +}); diff --git a/apps/expo/playwright/tests/fixtures.ts b/apps/expo/playwright/tests/fixtures.ts new file mode 100644 index 0000000000..edde2a6f35 --- /dev/null +++ b/apps/expo/playwright/tests/fixtures.ts @@ -0,0 +1,64 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { type Browser, type BrowserContext, test as base, type Page } from '@playwright/test'; + +const BASE_URL = process.env.BASE_URL ?? 'http://localhost:8081'; +export const API_URL = process.env.API_URL ?? 'http://localhost:8787'; + +const TOKENS_FILE = path.join(__dirname, '../.auth-tokens.json'); + +interface CachedAuth { + accessToken: string; + refreshToken: string; + user: Record | null; +} + +function loadCachedAuth(): CachedAuth { + if (!fs.existsSync(TOKENS_FILE)) { + throw new Error(`Auth tokens file not found at ${TOKENS_FILE}. Did globalSetup run?`); + } + // safe-cast: JSON.parse result is validated implicitly by the known file format written by globalSetup + return JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf-8')) as CachedAuth; +} + +/** + * Creates a browser context with auth pre-seeded in localStorage: + * - access_token / refresh_token → read by expo-sqlite kv-store stub + tokenAtom + * - user → read by ObservablePersistLocalStorage to hydrate userStore + * (isAuthed is computed from userStore !== null) + * + * Using storageState guarantees the values are present before ANY page JS runs. + */ +async function createAuthedContext(browser: Browser): Promise { + const { accessToken, refreshToken, user } = loadCachedAuth(); + + const localStorage = [ + { name: 'access_token', value: accessToken }, + { name: 'refresh_token', value: refreshToken }, + ]; + + if (user) { + localStorage.push({ name: 'user', value: JSON.stringify(user) }); + } + + return browser.newContext({ + storageState: { + cookies: [], + origins: [{ origin: BASE_URL, localStorage }], + }, + }); +} + +export type AuthFixtures = { authedPage: Page }; + +export const test = base.extend({ + authedPage: async ({ browser }, use) => { + const context = await createAuthedContext(browser); + const page = await context.newPage(); + await use(page); + await context.close(); + }, +}); + +export { expect } from '@playwright/test'; +export { BASE_URL }; diff --git a/apps/expo/playwright/tests/globalSetup.ts b/apps/expo/playwright/tests/globalSetup.ts new file mode 100644 index 0000000000..e711737424 --- /dev/null +++ b/apps/expo/playwright/tests/globalSetup.ts @@ -0,0 +1,116 @@ +/** + * Playwright global setup — runs once before all tests. + * + * Priority order for obtaining auth tokens: + * 1. TEST_ACCESS_TOKEN + TEST_REFRESH_TOKEN — used directly (no API call) + * 2. TEST_EMAIL + TEST_PASSWORD — logs in against the API (matches the + * iOS/Android Maestro pattern: seed the user, then log in with credentials) + * 3. Fallback — registers a fresh ephemeral user, reads the OTP from the DB, + * and verifies email to obtain tokens (useful for local development) + * + * The resulting tokens are written to .auth-tokens.json so the authedPage + * fixture can seed localStorage without hitting auth on every test. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { neon } from '@neondatabase/serverless'; + +const API_URL = process.env.API_URL ?? 'http://localhost:8787'; +const DB_URL = + process.env.NEON_DATABASE_URL ?? + '***REDACTED_DB_URL***'; + +export const TOKENS_FILE = path.join(__dirname, '../.auth-tokens.json'); + +async function setup() { + // Priority 1: pre-minted tokens provided directly + if (process.env.TEST_ACCESS_TOKEN && process.env.TEST_REFRESH_TOKEN) { + const meRes = await fetch(`${API_URL}/api/auth/me`, { + headers: { Authorization: `Bearer ${process.env.TEST_ACCESS_TOKEN}` }, + }); + const user = meRes.ok ? ((await meRes.json()) as { user: Record }).user : null; + fs.writeFileSync( + TOKENS_FILE, + JSON.stringify({ + accessToken: process.env.TEST_ACCESS_TOKEN, + refreshToken: process.env.TEST_REFRESH_TOKEN, + user, + }), + ); + console.log('[globalSetup] Using tokens from TEST_ACCESS_TOKEN env var'); + return; + } + + // Priority 2: log in with the seeded E2E user (CI path, matches iOS/Android pattern) + if (process.env.TEST_EMAIL && process.env.TEST_PASSWORD) { + const loginRes = await fetch(`${API_URL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: process.env.TEST_EMAIL, password: process.env.TEST_PASSWORD }), + }); + if (!loginRes.ok) { + const body = await loginRes.text(); + throw new Error(`Login failed ${loginRes.status}: ${body}`); + } + const { accessToken, refreshToken, user } = (await loginRes.json()) as { + accessToken: string; + refreshToken: string; + user: Record; + }; + fs.writeFileSync(TOKENS_FILE, JSON.stringify({ accessToken, refreshToken, user })); + console.log(`[globalSetup] Logged in as ${process.env.TEST_EMAIL}`); + return; + } + + // Priority 3: register a fresh ephemeral user (local dev fallback) + const email = `e2e-${Date.now()}@packrat.test`; + const password = 'E2eTest1!'; + + // 1. Register + const registerRes = await fetch(`${API_URL}/api/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, firstName: 'E2E', lastName: 'User' }), + }); + if (!registerRes.ok) { + const body = await registerRes.text(); + throw new Error(`Register failed ${registerRes.status}: ${body}`); + } + console.log(`[globalSetup] Registered ${email}`); + + // 2. Fetch OTP directly from the database + const sql = neon(DB_URL); + const rows = await sql` + SELECT otp.code + FROM one_time_passwords otp + JOIN users u ON u.id = otp.user_id + WHERE u.email = ${email} + ORDER BY otp.expires_at DESC + LIMIT 1 + `; + + const code = (rows[0] as { code: string } | undefined)?.code; + if (!code) throw new Error(`No OTP found in DB for ${email}`); + console.log(`[globalSetup] Got OTP from DB`); + + // 3. Verify email + const verifyRes = await fetch(`${API_URL}/api/auth/verify-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code }), + }); + if (!verifyRes.ok) { + const body = await verifyRes.text(); + throw new Error(`Verify failed ${verifyRes.status}: ${body}`); + } + const { accessToken, refreshToken, user } = (await verifyRes.json()) as { + accessToken: string; + refreshToken: string; + user: Record; + }; + console.log('[globalSetup] Email verified, tokens obtained'); + + fs.writeFileSync(TOKENS_FILE, JSON.stringify({ accessToken, refreshToken, user })); +} + +export default setup; diff --git a/apps/expo/playwright/tests/packs.spec.ts b/apps/expo/playwright/tests/packs.spec.ts new file mode 100644 index 0000000000..8b2421fa7f --- /dev/null +++ b/apps/expo/playwright/tests/packs.spec.ts @@ -0,0 +1,265 @@ +/** + * Web E2E tests for Pack and Item CRUD functionality. + * + * Covers: + * - Pack create / edit / delete + * - Item add (manually) / edit / delete + * - Validation: empty name on pack and item forms + * + * Auth is pre-seeded via the `authedPage` fixture (storageState). + * Pack IDs are always captured from the POST /api/packs response so that + * tests can navigate directly to detail/edit routes without relying on + * post-submit navigation behaviour. + */ +import { BASE_URL, expect, test } from './fixtures'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Create a pack via the UI and return its server-assigned id. */ +async function createPackViaForm( + page: import('@playwright/test').Page, + packName: string, +): Promise { + const [packResponse] = await Promise.all([ + page.waitForResponse((r) => r.url().includes('/api/packs') && r.request().method() === 'POST'), + (async () => { + await page.goto(`${BASE_URL}/pack/new`); + await page.getByTestId('packs:name-input').fill(packName); + await page.getByTestId('submit-pack-button').click(); + })(), + ]); + + expect(packResponse.ok()).toBeTruthy(); + const { id } = (await packResponse.json()) as { id: string }; + return id; +} + +/** Add an item to a pack via the UI, wait for the API to persist it, return item id. */ +async function addItemViaForm( + page: import('@playwright/test').Page, + opts: { packId: string; itemName: string; weight?: string }, +): Promise { + const { packId, itemName, weight = '500' } = opts; + await page.goto(`${BASE_URL}/item/new?packId=${packId}`); + await page.getByTestId('items:name-input').fill(itemName); + await page.getByTestId('items:weight-input').fill(weight); + + const itemPostPromise = page.waitForResponse( + (r) => + r.url().includes('/api/packs') && + r.url().includes('/items') && + r.request().method() === 'POST', + { timeout: 20_000 }, + ); + + await page.getByTestId('items:submit').click(); + const response = await itemPostPromise; + expect(response.ok()).toBeTruthy(); + const body = (await response.json()) as { id: string }; + return body.id; +} + +// ─── Pack CRUD ──────────────────────────────────────────────────────────────── + +test.describe('Pack CRUD', () => { + test('create pack → appears in packs list', async ({ authedPage: page }) => { + test.setTimeout(30_000); + const packName = `E2E-Create-${Date.now()}`; + + await createPackViaForm(page, packName); + + await page.goto(`${BASE_URL}/packs`); + await expect(page.getByText(packName)).toBeVisible({ timeout: 10_000 }); + }); + + test('edit pack name → updated name appears in detail', async ({ authedPage: page }) => { + test.setTimeout(60_000); + const originalName = `E2E-Edit-${Date.now()}`; + const updatedName = `${originalName}-UPDATED`; + + const packId = await createPackViaForm(page, originalName); + + // Use the header edit button (SPA nav) so router.back() stays in-SPA and + // syncedCrud can flush the PUT before the page unloads. + await page.goto(`${BASE_URL}/pack/${packId}`); + await page.waitForLoadState('networkidle'); + await page.getByTestId('packs:edit').click(); + + const nameInput = page.getByTestId('packs:name-input'); + await nameInput.waitFor({ timeout: 10_000 }); + await nameInput.clear(); + await nameInput.fill(updatedName); + + // Register listener before clicking — scoped to this pack's URL + const editPutPromise = page.waitForResponse( + (r) => + r.url().includes(`/api/packs/${packId}`) && + (r.request().method() === 'PUT' || r.request().method() === 'PATCH'), + { timeout: 20_000 }, + ); + + await page.getByTestId('submit-pack-button').click(); + + // SPA router.back() keeps the JS context alive; await the PUT before navigating away + await editPutPromise; + + // Updated name should appear in the pack detail (full reload from API) + await page.goto(`${BASE_URL}/pack/${packId}`); + await expect(page.getByText(updatedName)).toBeVisible({ timeout: 10_000 }); + }); + + test('delete pack → disappears from packs list', async ({ authedPage: page }) => { + test.setTimeout(60_000); + const packName = `E2E-Delete-${Date.now()}`; + const packId = await createPackViaForm(page, packName); + + await page.goto(`${BASE_URL}/pack/${packId}`); + + // Wait for the store to load and the owner check to resolve so header buttons appear + await page.waitForLoadState('networkidle'); + + // Accept any browser-native confirm/alert dialogs before triggering delete + page.on('dialog', (dialog) => dialog.accept()); + + const deleteButton = page.getByTestId('packs:delete'); + await deleteButton.waitFor({ timeout: 15_000 }); + await deleteButton.click(); + + // After deletion the app should navigate away; go to list and confirm pack is gone + await page.goto(`${BASE_URL}/packs`); + await expect(page.getByText(packName)).not.toBeVisible({ timeout: 10_000 }); + }); +}); + +// ─── Item CRUD within a pack ────────────────────────────────────────────────── + +test.describe('Item CRUD within a pack', () => { + // Create a fresh pack before each item test so tests are independent + let sharedPackId: string; + + test.beforeEach(async ({ authedPage: page }) => { + const packName = `E2E-ItemPack-${Date.now()}`; + sharedPackId = await createPackViaForm(page, packName); + }); + + test('add item manually → appears in pack detail', async ({ authedPage: page }) => { + test.setTimeout(60_000); + const itemName = `E2E-Item-${Date.now()}`; + + await addItemViaForm(page, { packId: sharedPackId, itemName, weight: '850' }); + + await page.goto(`${BASE_URL}/pack/${sharedPackId}`); + await expect(page.getByText(itemName)).toBeVisible({ timeout: 15_000 }); + }); + + test('edit item name → updated name appears in pack detail', async ({ authedPage: page }) => { + test.setTimeout(90_000); + const itemName = `E2E-EditItem-${Date.now()}`; + const updatedItemName = `${itemName}-UPDATED`; + + const itemId = await addItemViaForm(page, { packId: sharedPackId, itemName, weight: '500' }); + + // Navigate to pack detail to verify item exists + await page.goto(`${BASE_URL}/pack/${sharedPackId}`); + await expect(page.getByText(itemName)).toBeVisible({ timeout: 15_000 }); + + // Navigate to the item edit form + await page.goto(`${BASE_URL}/item/${itemId}/edit?packId=${sharedPackId}`); + const nameInput = page.getByTestId('items:name-input'); + await nameInput.waitFor({ timeout: 10_000 }); + await nameInput.clear(); + await nameInput.fill(updatedItemName); + + const editPromise = page.waitForResponse( + (r) => + r.url().includes('/api/packs') && + r.url().includes('/items') && + (r.request().method() === 'PUT' || r.request().method() === 'PATCH'), + { timeout: 20_000 }, + ); + + await page.getByTestId('items:submit').click(); + await editPromise.catch(() => null); + + // Updated name should be visible in pack detail + await page.goto(`${BASE_URL}/pack/${sharedPackId}`); + await expect(page.getByText(updatedItemName)).toBeVisible({ timeout: 15_000 }); + }); + + test('delete item via more-actions menu → disappears from pack detail', async ({ + authedPage: page, + }) => { + test.setTimeout(90_000); + const itemName = `E2E-DeleteItem-${Date.now()}`; + + const itemId = await addItemViaForm(page, { packId: sharedPackId, itemName, weight: '300' }); + + // Confirm item is in pack detail + await page.goto(`${BASE_URL}/pack/${sharedPackId}`); + await expect(page.getByTestId(`items:card-${itemId}`)).toBeVisible({ timeout: 15_000 }); + + // Accept dialogs (web confirm) before triggering delete + page.on('dialog', (dialog) => dialog.accept()); + + // Open the more-actions menu for the item + const moreActionsButton = page.getByTestId(`items:more-actions-${itemId}`); + if (await moreActionsButton.isVisible()) { + await moreActionsButton.click(); + const deleteOption = page + .getByText(/delete/i) + .or(page.getByRole('menuitem', { name: /delete/i })) + .first(); + await deleteOption.waitFor({ timeout: 5_000 }); + await deleteOption.click(); + + // Item card should be gone + await expect(page.getByTestId(`items:card-${itemId}`)).not.toBeVisible({ timeout: 10_000 }); + } else { + test.skip(true, 'items:more-actions button not accessible on web'); + } + }); +}); + +// ─── Validation ─────────────────────────────────────────────────────────────── + +test.describe('Validation', () => { + test.setTimeout(30_000); + + test('empty pack name → form does not navigate on submit', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/pack/new`); + + const submitButton = page.getByTestId('submit-pack-button'); + await submitButton.waitFor({ timeout: 10_000 }); + + // Name field starts empty — clicking submit should either be blocked or stay on this page + const formUrl = page.url(); + await submitButton.click(); + + // Wait a moment for any navigation to settle + await page.waitForTimeout(1_000); + + // Should still be on the create form (validation prevented navigation) + expect(page.url()).toBe(formUrl); + }); + + test('empty item name → form does not navigate on submit', async ({ authedPage: page }) => { + const packId = await createPackViaForm(page, `E2E-Validation-${Date.now()}`); + + await page.goto(`${BASE_URL}/item/new?packId=${packId}`); + + const submitButton = page.getByTestId('items:submit'); + await submitButton.waitFor({ timeout: 10_000 }); + + const nameInput = page.getByTestId('items:name-input'); + await nameInput.waitFor({ timeout: 10_000 }); + await nameInput.clear(); + + const formUrl = page.url(); + await submitButton.click(); + + await page.waitForTimeout(1_000); + + // Should still be on the create item form + expect(page.url()).toBe(formUrl); + }); +}); diff --git a/apps/expo/playwright/tests/profile.spec.ts b/apps/expo/playwright/tests/profile.spec.ts new file mode 100644 index 0000000000..c182c6a29e --- /dev/null +++ b/apps/expo/playwright/tests/profile.spec.ts @@ -0,0 +1,132 @@ +/** + * Web E2E tests for PackRat profile functionality. + * + * Tests use the `authedPage` fixture which pre-seeds auth tokens in + * localStorage before any page JS runs. + * + * TestIds match the constants in lib/testIds.ts. + */ +import { testIds } from '../../lib/testIds'; +import { BASE_URL, expect, test } from './fixtures'; + +// ─── Profile name edit ──────────────────────────────────────────────────────── + +test.describe('Profile name edit', () => { + test('both name inputs are visible on /profile/name', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/profile/name`); + + await expect(page.getByTestId(testIds.profile.firstNameInput)).toBeVisible(); + await expect(page.getByTestId(testIds.profile.lastNameInput)).toBeVisible(); + }); + + test('save button is disabled when name is unchanged', async ({ authedPage: page }) => { + await page.goto(`${BASE_URL}/profile/name`); + + const saveBtn = page.getByTestId(testIds.profile.saveBtn); + await saveBtn.waitFor({ state: 'visible' }); + + // NativeWindUI Button renders as
on web, not