From 0c82701955e891c25522b49063fa5c5f9fdeabb7 Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 15:23:01 +0000 Subject: [PATCH 01/18] test: ship without scanning --- beam/tests/mobile/test_shipping.py | 109 +++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 beam/tests/mobile/test_shipping.py diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py new file mode 100644 index 00000000..2c6f6647 --- /dev/null +++ b/beam/tests/mobile/test_shipping.py @@ -0,0 +1,109 @@ + +# **Complete a partial shipment** +# As a warehouse employee, I want to scan items from a Sales Order and create a partial Delivery Note so that I can ship available inventory without waiting for the full order. + +# - Navigate Home → Ship → select Sales Order +# - Scan item barcode, count increments +# - Click SAVE, draft Delivery Note created +# - Click SHIP, Delivery Note submitted +# - Remaining qty still available for future shipment + +# **Cancel a submitted Delivery Note** +# As a warehouse employee, I want to cancel a submitted Delivery Note so that I can correct mistakes made during shipping. + +# - Navigate to a submitted Delivery Note +# - CANCEL button visible, SHIP button hidden +# - Click CANCEL, docstatus changes to Cancelled +# - Stock ledger entries reversed + +# **Scan handling unit on Delivery Note** +# As a warehouse employee, I want to scan a handling unit barcode instead of an item barcode so that I can ship entire pallets/containers efficiently. + +# - Scan HU barcode on Delivery Note form +# - Item automatically identified from HU +# - Quantity populated from HU stock qty +# - Handling unit field populated on line item + +# **Prevent over-delivery** +# As a warehouse employee, I want the system to prevent me from shipping more than the ordered quantity so that I don't accidentally over-ship. + +# - Scan item barcode repeatedly past ordered qty +# - Count stops at ordered qty OR warning displayed +# - Cannot submit with qty exceeding Sales Order line + +# **Ship with unsaved changes warning** +# As a warehouse employee, I want to be warned if I navigate away with unsaved scans so that I don't lose my work. + +# - Scan items on Delivery Note +# - "Unsaved" indicator visible in header +# - Attempt to navigate to Home +# - Warning or confirmation displayed + +# To test locally: +# active the virtual environment +# bench start, and then run: +# pytest ./beam/tests/test_shipping.py --browser firefox --headed --disable-warnings + +import re +from urllib.parse import urlparse + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + + +@pytest.mark.order(15) +def test_ship_without_scanning(page): + """Test trying to ship without scanning any items""" + # navigate to Ship -> Sales Order + page.get_by_text("Ship").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Sales Order + parsed_url = urlparse(page.url.replace("#", "")) + path_parts = [p for p in parsed_url.path.split("/") if p] + order_id = path_parts[-1] if path_parts else None + assert order_id + + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + + # find all items in the list + all_item_counts = page.locator("css=.box .beam_item-count") + initial_counts = [] + for i in range(all_item_counts.count()): + count_text = all_item_counts.nth(i).inner_text() + initial_counts.append(count_text) + + # ensure all items start with 0 count + for count in initial_counts: + assert count.startswith("0/"), f"Expected item to start with 0/, but got: {count}" + + # count existing Delivery Notes before attempting to save + with use_current_db_transaction(): + existing_notes = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code, "owner": "support@agritheory.dev"}, + fields=["docstatus", "qty"], + ) + initial_count = len(existing_notes) + + # try to click SAVE without scanning anything + save_button = page.get_by_text("SAVE", exact=True) + save_button.click() + page.wait_for_timeout(1000) + + # verify no new draft Delivery Note was created + with use_current_db_transaction(): + new_notes = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code, "owner": "support@agritheory.dev"}, + fields=["docstatus", "qty"], + ) + final_count = len(new_notes) + assert ( + final_count == initial_count + ), f"Expected no new delivery notes, but count changed from {initial_count} to {final_count}" + From 0bdb21ef31b747cc8027702525ae2f44ac4cb30c Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 15:25:03 +0000 Subject: [PATCH 02/18] test: complete partial shipment --- beam/tests/mobile/test_shipping.py | 93 ++++++++++++++++++++++++++++++ beam/tests/setup.py | 30 ++++++++++ 2 files changed, 123 insertions(+) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index 2c6f6647..55622097 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -107,3 +107,96 @@ def test_ship_without_scanning(page): final_count == initial_count ), f"Expected no new delivery notes, but count changed from {initial_count} to {final_count}" + +@pytest.mark.order(16) +def test_complete_partial_shipment(page): + """Test completing a partial shipment""" + page.get_by_text("Ship").click() + page.locator("css=.beam_list-item").first.click() + + parsed_url = urlparse(page.url.replace("#", "")) + order_id = parsed_url.query.replace("id=", "") + assert order_id + + # find the first item in the list + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + expect(item_count).to_have_text(re.compile("0/")) + + with use_current_db_transaction(): + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # get the ordered quantity for validation + so_items = frappe.get_all( + "Sales Order Item", + filters={"parent": order_id, "item_code": item_code}, + fields=["qty", "delivered_qty"], + ) + + assert len(so_items) > 0 + ordered_qty = so_items[0]["qty"] + delivered_qty = so_items[0]["delivered_qty"] + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + expect(item_count).to_have_text(re.compile("1/")) + + # ensure there are no existing Delivery Notes against this Sales Order for this item + delivery_note = frappe.db.exists( + "Delivery Note Item", + { + "docstatus": 0, + "against_sales_order": order_id, + "item_code": item_code, + "owner": "support@agritheory.dev", + }, + ) + assert not delivery_note + + # check that a draft Delivery Note is created + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + with use_current_db_transaction(): + delivery_note = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code}, + fields=["docstatus", "qty", "creation", "parent"], + order_by="creation desc", + ) + assert len(delivery_note) >= 1 + assert delivery_note[0]["docstatus"] == 0 + assert delivery_note[0]["qty"] == 1 + + # check that the draft Delivery Note is submitted + page.get_by_text("SHIP", exact=True).click() + page.wait_for_timeout(1500) + with use_current_db_transaction(): + delivery_note = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code}, + fields=["docstatus", "qty", "creation"], + order_by="creation desc", + limit=1, + ) + assert len(delivery_note) == 1 + assert delivery_note[0]["docstatus"] == 1 + assert delivery_note[0]["qty"] == 1 + + # verify remaining qty is still available for future shipment + with use_current_db_transaction(): + so_items = frappe.get_all( + "Sales Order Item", + filters={"parent": order_id, "item_code": item_code}, + fields=["qty", "delivered_qty"], + ) + assert len(so_items) > 0 + new_delivered_qty = so_items[0]["delivered_qty"] + assert new_delivered_qty == delivered_qty + 1 + assert new_delivered_qty < ordered_qty, "Should still have remaining qty available" + diff --git a/beam/tests/setup.py b/beam/tests/setup.py index a437efd0..df561163 100644 --- a/beam/tests/setup.py +++ b/beam/tests/setup.py @@ -98,6 +98,7 @@ def create_test_data(): create_employees(settings) create_items(settings) create_boms(settings) + create_finished_goods_stock(settings) prod_plan_from_doc = "Sales Order" if prod_plan_from_doc == "Sales Order": create_sales_order(settings) @@ -354,6 +355,35 @@ def create_items(settings): water.submit() +def create_finished_goods_stock(settings): + """Create initial stock of finished goods for testing. + Used by test_shipping.py::test_complete_partial_shipment and other shipping tests. + """ + finished_goods_items = [ + {"item_code": "Ambrosia Pie", "qty": 50, "rate": 10.00}, + {"item_code": "Double Plum Pie", "qty": 50, "rate": 9.00}, + {"item_code": "Gooseberry Pie", "qty": 50, "rate": 12.00}, + {"item_code": "Kaduka Key Lime Pie", "qty": 50, "rate": 9.00}, + ] + + for item_data in finished_goods_items: + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.stock_entry_type = stock_entry.purpose = "Material Receipt" + stock_entry.append( + "items", + { + "item_code": item_data["item_code"], + "qty": item_data["qty"], + "t_warehouse": "Baked Goods - APC", + "uom": "Nos", + "basic_rate": item_data["rate"], + "expense_account": "5111 - Cost of Goods Sold - APC", + }, + ) + stock_entry.save() + stock_entry.submit() + + def create_warehouses(settings): warehouses = [item.get("default_warehouse") for item in items] root_wh = frappe.get_value("Warehouse", {"company": settings.company, "is_group": 1}) From 1ca88664c47841dcfe9dfc7c5697cc38e5aa9523 Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 15:25:57 +0000 Subject: [PATCH 03/18] fix: wrogn validation --- beam/www/beam/pages/DeliveryNote.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beam/www/beam/pages/DeliveryNote.vue b/beam/www/beam/pages/DeliveryNote.vue index 884a7e66..f08e1980 100644 --- a/beam/www/beam/pages/DeliveryNote.vue +++ b/beam/www/beam/pages/DeliveryNote.vue @@ -67,7 +67,7 @@ const create = async () => { } const { data, response } = await store.insert('Delivery Note', document) - if (!response.ok) { + if (response.ok) { store.$patch(() => { deliveryNote.value = data deliveryNote.value.dirty = false From cdbd7bf7b1660153bcc7a19ce525ce87ba89514a Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 15:26:35 +0000 Subject: [PATCH 04/18] test: prevent over delivery --- beam/tests/mobile/test_shipping.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index 55622097..35ed9d85 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -200,3 +200,63 @@ def test_complete_partial_shipment(page): assert new_delivered_qty == delivered_qty + 1 assert new_delivered_qty < ordered_qty, "Should still have remaining qty available" + +@pytest.mark.order(17) +def test_prevent_over_delivery(page): + """Test that system prevents over-delivery beyond ordered quantity""" + page.get_by_text("Ship").click() + page.locator("css=.beam_list-item").first.click() + + parsed_url = urlparse(page.url.replace("#", "")) + order_id = parsed_url.query.replace("id=", "") + assert order_id + + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + + # get the ordered quantity and remaining quantity + with use_current_db_transaction(): + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + so_items = frappe.get_all( + "Sales Order Item", + filters={"parent": order_id, "item_code": item_code}, + fields=["qty", "delivered_qty"], + ) + assert len(so_items) > 0 + ordered_qty = so_items[0]["qty"] + delivered_qty = so_items[0]["delivered_qty"] + remaining_qty = ordered_qty - delivered_qty + + assert remaining_qty > 0 + + # scan barcode beyond the remaining quantity + scan_attempts = int(remaining_qty) + 5 # try to scan 5 more than allowed + for i in range(scan_attempts): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + page.wait_for_timeout(100) + + page.wait_for_timeout(500) + + count_text = item_count.inner_text() + current_count = int(count_text.split("/")[0]) + assert current_count <= remaining_qty, f"Count {current_count} should not exceed remaining qty {remaining_qty}" + + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + + with use_current_db_transaction(): + notes = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code}, + fields=["qty"], + order_by="creation desc", + limit=1, + ) + if len(notes) > 0: + assert notes[0]["qty"] <= remaining_qty, "Delivery Note qty should not exceed remaining qty" + From 2466561cbd87e9ab7b8fb13c8fd731d68812b3b8 Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 15:26:55 +0000 Subject: [PATCH 05/18] fix: limit scan qty --- beam/www/beam/stores/scan.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/beam/www/beam/stores/scan.ts b/beam/www/beam/stores/scan.ts index 3392c919..e04115aa 100644 --- a/beam/www/beam/stores/scan.ts +++ b/beam/www/beam/stores/scan.ts @@ -122,7 +122,11 @@ export const useScanStore = defineStore('scan', () => { if (existing_rows.length > 0) { const field = itemQtyFieldMap[action.doctype] || 'qty' for (const row of existing_rows) { - row[field] = row[field] + 1 + if (row.qty) { + row[field] = Math.min(row[field] + 1, row.qty) + } else { + row[field] = row[field] + 1 + } } } else if (action.doctype === 'Stock Entry') { const source_warehouses = ['Material Consumption for Manufacture', 'Material Issue'] From 9ad6952860cb67e188ecd2737a52c0ee3720a6e9 Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 16:46:19 +0000 Subject: [PATCH 06/18] fix: find order_id --- beam/tests/mobile/test_shipping.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index 35ed9d85..c78b2e0f 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -61,10 +61,8 @@ def test_ship_without_scanning(page): page.get_by_text("Ship").click() page.locator("css=.beam_list-item").first.click() - # get the selected Sales Order parsed_url = urlparse(page.url.replace("#", "")) - path_parts = [p for p in parsed_url.path.split("/") if p] - order_id = path_parts[-1] if path_parts else None + order_id = parsed_url.query.replace("id=", "") assert order_id item = page.locator("css=.box .beam_list-item").first From 133c231e834906b73fff677c651bc05567e2271a Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 16:58:03 +0000 Subject: [PATCH 07/18] feat: submit and cancel fn --- beam/www/beam/pages/DeliveryNote.vue | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/beam/www/beam/pages/DeliveryNote.vue b/beam/www/beam/pages/DeliveryNote.vue index f08e1980..77fc69ed 100644 --- a/beam/www/beam/pages/DeliveryNote.vue +++ b/beam/www/beam/pages/DeliveryNote.vue @@ -80,6 +80,26 @@ const create = async () => { } } +const submit = async () => { + const { data, response } = await store.submit('Delivery Note', deliveryNote.value.name) + if (response.ok) { + store.$patch(() => { + deliveryNote.value = data + deliveryNote.value.dirty = false + }) + } +} + +const cancel = async () => { + const { data, response } = await store.cancel('Delivery Note', deliveryNote.value.name) + if (response.ok) { + store.$patch(() => { + deliveryNote.value = data + deliveryNote.value.dirty = false + }) + } +} + const controlButtons = computed((): ControlButton[] => { if (!deliveryNote.value) return [] @@ -98,14 +118,14 @@ const controlButtons = computed((): ControlButton[] => { disabled: form.items.length === 0 || !form.name, hidden: Boolean(form.__islocal) || form.docstatus !== 0, color: { background: 'var(--sc-success)', text: 'var(--sc-btn-color)' }, - action: async () => await store.submit('Delivery Note', form.name), + action: submit, }, { label: 'CANCEL', disabled: form.items.length === 0 || !form.name, hidden: Boolean(form.__islocal) || form.docstatus !== 1, color: { background: 'var(--sc-alert)', text: 'var(--sc-btn-color)' }, - action: async () => await store.cancel('Delivery Note', form.name), + action: cancel, }, ] }) From d15618ab446f916372951e341996a8bde2514b2e Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 17:17:34 +0000 Subject: [PATCH 08/18] feat: clean delivery note --- beam/tests/mobile/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beam/tests/mobile/conftest.py b/beam/tests/mobile/conftest.py index 20ef8000..a9a93554 100644 --- a/beam/tests/mobile/conftest.py +++ b/beam/tests/mobile/conftest.py @@ -20,7 +20,7 @@ def browser_context_args(browser_context_args): @pytest.fixture(autouse=True) def setup(page): # delete all existing draft Purchase Receipts - delete_draft_records(["Purchase Receipt", "Stock Entry"]) + delete_draft_records(["Purchase Receipt", "Stock Entry", "Delivery Note"]) page.set_default_timeout(5000) From 07e04c52d7e9398237792a9098fa884380c9948f Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 17:50:34 +0000 Subject: [PATCH 09/18] test: cancel submitted shipping --- beam/tests/mobile/test_shipping.py | 87 ++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index c78b2e0f..d43cc95e 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -258,3 +258,90 @@ def test_prevent_over_delivery(page): if len(notes) > 0: assert notes[0]["qty"] <= remaining_qty, "Delivery Note qty should not exceed remaining qty" + +@pytest.mark.order(18) +def test_cancel_submitted_delivery_note_workflow(page): + """Test cancelling a submitted Delivery Note through the complete workflow""" + page.get_by_text("Ship").click() + page.locator("css=.beam_list-item").first.click() + + parsed_url = urlparse(page.url.replace("#", "")) + order_id = parsed_url.query.replace("id=", "") + assert order_id + + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + + with use_current_db_transaction(): + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + page.wait_for_timeout(500) + + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + + # Verify draft Delivery Note was created + with use_current_db_transaction(): + delivery_notes = frappe.get_all( + "Delivery Note Item", + filters={"against_sales_order": order_id, "item_code": item_code}, + fields=["docstatus", "parent"], + order_by="creation desc", + limit=1, + ) + assert len(delivery_notes) > 0 + assert delivery_notes[0]["docstatus"] == 0 + dn_name = delivery_notes[0]["parent"] + + # Submit the Delivery Note + ship_button = page.get_by_text("SHIP", exact=True) + expect(ship_button).to_be_visible() + ship_button.click() + page.wait_for_timeout(1500) + + # Verify Delivery Note is submitted + with use_current_db_transaction(): + dn = frappe.get_doc("Delivery Note", dn_name) + assert dn.docstatus == 1, f"Expected docstatus 1 (Submitted), got {dn.docstatus}" + + # Verify CANCEL button is visible and SHIP button is hidden + cancel_button = page.get_by_text("CANCEL", exact=True) + expect(cancel_button).to_be_visible() + expect(ship_button).not_to_be_visible() + + with use_current_db_transaction(): + sle_before = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_type": "Delivery Note", "voucher_no": dn_name}, + fields=["name", "actual_qty"], + ) + sle_count_before = len(sle_before) + assert sle_count_before > 0, "Should have stock ledger entries after submission" + + # Click CANCEL button + cancel_button.click() + page.wait_for_timeout(1500) + + with use_current_db_transaction(): + dn = frappe.get_doc("Delivery Note", dn_name) + assert dn.docstatus == 2, f"Expected docstatus 2 (Cancelled), got {dn.docstatus}" + + # Verify stock ledger entries were reversed + sle_after = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_type": "Delivery Note", "voucher_no": dn_name}, + fields=["name", "actual_qty"], + ) + sle_count_after = len(sle_after) + + # Should have double the entries (original + reversal) + assert sle_count_after == sle_count_before * 2, f"Expected {sle_count_before * 2} SLE entries, got {sle_count_after}" + + expect(cancel_button).not_to_be_visible() From ff128a07988731ad8729056bf6ea5f96a093f900 Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 17:52:04 +0000 Subject: [PATCH 10/18] test: skeletton for cancel when navigate directly to delivery-note --- beam/tests/mobile/test_shipping.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index d43cc95e..6eac7f38 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -345,3 +345,28 @@ def test_cancel_submitted_delivery_note_workflow(page): assert sle_count_after == sle_count_before * 2, f"Expected {sle_count_before * 2} SLE entries, got {sle_count_after}" expect(cancel_button).not_to_be_visible() + + +@pytest.mark.order(19) +@pytest.mark.skip(reason="Frontend does not load docstatus when navigating directly to delivery-note URL") +def test_cancel_submitted_delivery_note(page): + """Test cancelling a submitted Delivery Note""" + with use_current_db_transaction(): + submitted_notes = frappe.get_all( + "Delivery Note", + filters={"docstatus": 1, "owner": "support@agritheory.dev"}, + fields=["name"], + order_by="creation desc", + limit=1, + ) + + assert len(submitted_notes) > 0, "Should have at least one submitted Delivery Note from previous tests" + dn_name = submitted_notes[0]["name"] + + base_url = frappe.utils.get_url() + page.goto(f"{base_url}/app/delivery-note/{dn_name}") + page.wait_for_timeout(1000) + + cancel_button = page.get_by_role("button", name="Cancel") + expect(cancel_button).to_be_visible() + From af7528118c498c5566645c62e6be48891ab0b84d Mon Sep 17 00:00:00 2001 From: lauty95 Date: Fri, 20 Feb 2026 17:56:58 +0000 Subject: [PATCH 11/18] test: ship with unsaved changes warning --- beam/tests/mobile/test_shipping.py | 59 ++++++++++++++++++++++++++++ beam/www/beam/pages/DeliveryNote.vue | 17 +++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/beam/tests/mobile/test_shipping.py b/beam/tests/mobile/test_shipping.py index 6eac7f38..dac9ce64 100644 --- a/beam/tests/mobile/test_shipping.py +++ b/beam/tests/mobile/test_shipping.py @@ -370,3 +370,62 @@ def test_cancel_submitted_delivery_note(page): cancel_button = page.get_by_role("button", name="Cancel") expect(cancel_button).to_be_visible() + +@pytest.mark.order(20) +def test_unsaved_changes_warning(page): + """Test that user is warned when navigating away with unsaved changes""" + page.get_by_text("Ship").click() + page.locator("css=.beam_list-item").first.click() + + parsed_url = urlparse(page.url.replace("#", "")) + order_id = parsed_url.query.replace("id=", "") + assert order_id + + unsaved_indicator = page.locator("span.dirty") + expect(unsaved_indicator).not_to_be_visible() + + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + + with use_current_db_transaction(): + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # Scan barcode to create unsaved changes + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + page.wait_for_timeout(500) + + expect(unsaved_indicator).to_be_visible() + expect(unsaved_indicator).to_have_text("Unsaved") + + should_accept = [False] + def handle_dialog(dialog): + if should_accept[0]: + dialog.accept() + else: + dialog.dismiss() + + page.on("dialog", handle_dialog) + + # First attempt: dismiss the dialog + home_link = page.get_by_role("link", name="Home") + home_link.click() + page.wait_for_timeout(500) + + # Verify we stayed on the same page (dialog was shown and dismissed) + assert "delivery-note" in page.url, "Should still be on delivery-note page after dismissing warning" + + # Second attempt: accept the dialog + should_accept[0] = True + home_link.click() + page.wait_for_timeout(500) + + # Verify we navigated away (dialog was shown and accepted) + assert "delivery-note" not in page.url, "Should have left delivery-note page after accepting warning" + + diff --git a/beam/www/beam/pages/DeliveryNote.vue b/beam/www/beam/pages/DeliveryNote.vue index 77fc69ed..3790abc4 100644 --- a/beam/www/beam/pages/DeliveryNote.vue +++ b/beam/www/beam/pages/DeliveryNote.vue @@ -23,7 +23,7 @@