From 01f553935d9f32e2020a66ca20ef8d6155c6a63d Mon Sep 17 00:00:00 2001 From: Samuel Miserendino Date: Wed, 19 Feb 2025 23:25:22 -0800 Subject: [PATCH] Start adding December tasks, update tooling API --- issues/24642_386/user_tool.py | 30 ++- issues/25901_945/user_tool.py | 4 +- issues/26004_979/user_tool.py | 42 ++--- issues/26228_719/user_tool.py | 49 ++--- issues/26648_972/user_tool.py | 34 ++-- issues/27156_665/user_tool.py | 56 +++--- issues/28021_804/user_tool.py | 38 ++-- issues/28077_782/user_tool.py | 40 ++-- issues/28087_800/user_tool.py | 51 +++--- issues/28092_839/user_tool.py | 4 +- issues/28943_641/user_tool.py | 19 +- issues/29163_589/user_tool.py | 4 +- issues/29222_720/user_tool.py | 28 ++- issues/29598_896/user_tool.py | 50 ++--- issues/29854_602/user_tool.py | 53 +++--- issues/30195_650/user_tool.py | 44 ++--- issues/37688_441/user_tool.py | 61 +++---- issues/37766_765/user_tool.py | 47 ++--- issues/38147_954/user_tool.py | 42 ++--- issues/38163_970/user_tool.py | 21 ++- issues/39614_1101/user_tool.py | 35 ++-- issues/41529_679/user_tool.py | 4 +- issues/41613_529/user_tool.py | 79 ++++---- issues/41771_458/user_tool.py | 53 ++---- issues/42556_686/user_tool.py | 49 ++--- issues/42934_932/user_tool.py | 85 ++++----- issues/44040_470/user_tool.py | 87 +++++---- issues/44509_397/user_tool.py | 38 ++-- issues/44728_421/user_tool.py | 23 +-- issues/44804_694/user_tool.py | 25 ++- issues/45190_520/user_tool.py | 21 ++- issues/45523_892/user_tool.py | 23 +-- issues/45543_1016/user_tool.py | 52 +++--- issues/45882_428/user_tool.py | 62 +++---- issues/46053_566/user_tool.py | 39 +--- issues/46344_753/user_tool.py | 67 +++---- issues/46371_1127/user_tool.py | 55 +++--- issues/46383_999/user_tool.py | 43 ++--- issues/48694_681/user_tool.py | 252 +++++++++++++------------- issues/49260_543/user_tool.py | 3 +- issues/49298_510/user_tool.py | 45 +++-- issues/49492_670/user_tool.py | 27 +-- issues/49543_647/user_tool.py | 4 +- issues/49933_1062/user_tool.py | 4 +- issues/49993_996/user_tool.py | 27 +-- issues/50577_1094/user_tool.py | 55 ++---- issues/53036/commit_id.txt | 1 + issues/53036/flow.mitm | Bin 0 -> 295522 bytes issues/53036/issue_data.json | 5 + issues/53036/issue_introduction.patch | 0 issues/53036/revert_command.txt | 1 + issues/53036/test.py | 128 +++++++++++++ issues/53036/user_tool.py | 133 ++++++++++++++ 53 files changed, 1118 insertions(+), 1124 deletions(-) create mode 100644 issues/53036/commit_id.txt create mode 100644 issues/53036/flow.mitm create mode 100644 issues/53036/issue_data.json create mode 100644 issues/53036/issue_introduction.patch create mode 100644 issues/53036/revert_command.txt create mode 100644 issues/53036/test.py create mode 100644 issues/53036/user_tool.py diff --git a/issues/24642_386/user_tool.py b/issues/24642_386/user_tool.py index 45edc30..73c7d0a 100644 --- a/issues/24642_386/user_tool.py +++ b/issues/24642_386/user_tool.py @@ -12,7 +12,6 @@ import email import re import random - from utils.trace_cleaner import trace_cleaner @@ -69,22 +68,18 @@ def get_magic_code(user_email, password, since=None, retries=5, delay=10): def login_user_ND_with_magiccode(user_email, password, page): - - page.goto(NEWDOT_URL) - page.locator('input[type="email"]').fill(user_email) page.locator("div:nth-child(3) > div:nth-child(2) > div > div > div > .css-view-175oi2r").click() page.wait_for_timeout(5000) - - magic_code = "123456" - + page.locator('input[autocomplete="one-time-code"]').fill(magic_code) page.wait_for_timeout(500) + def test_issue_24642(): @@ -105,14 +100,15 @@ def test_issue_24642(): context_args["geolocation"] = {"latitude": 37.7749, "longitude": -122.4194} context_args["permissions"] = ["geolocation"] context_args["ignore_https_errors"] = True - + context = browser.new_context(**context_args) + # Start tracing with screenshots, snapshots and sources context.tracing.start(screenshots=True, snapshots=True, sources=True) - try: - page = context.new_page() - first_user_email = generate_random_email() + page = context.new_page() + first_user_email = generate_random_email() + try: login_user_ND_with_magiccode(first_user_email, PASSWORD, page) page.wait_for_timeout(2000) context.set_offline(True) @@ -121,16 +117,16 @@ def test_issue_24642(): page.get_by_text("Personal details").click() try: page.get_by_role("button", name="Back").click(timeout=10000) - except Exception as e: pytest.fail("Test failed due to an exception") - - except Exception as main_ex: - raise main_ex finally: - context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path) + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) + context.close() browser.close() + + +if __name__ == "__main__": + test_issue_24642() \ No newline at end of file diff --git a/issues/25901_945/user_tool.py b/issues/25901_945/user_tool.py index 54d631f..d29372b 100644 --- a/issues/25901_945/user_tool.py +++ b/issues/25901_945/user_tool.py @@ -79,8 +79,8 @@ async def test_issue_25901(): finally: - await context.tracing.stop() - await context.tracing.export(path="/app/expensify/user_tool/output_browser1.zip") + + await context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") trace_cleaner("/app/expensify/user_tool/output_browser1.zip") diff --git a/issues/26004_979/user_tool.py b/issues/26004_979/user_tool.py index 371a1b3..5ecacc8 100644 --- a/issues/26004_979/user_tool.py +++ b/issues/26004_979/user_tool.py @@ -3,37 +3,34 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import time from playwright.sync_api import sync_playwright, expect from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page -from utils.trace_cleaner import trace_cleaner # <--- Added import - def generate_random_email(): task_number = 26004 return f"jellystarfish+{task_number}@gmail.com" -def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, str]: +def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, object, str]: browser = p.chromium.launch(headless=True, args=["--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"]) context = browser.new_context(ignore_https_errors=True) page = context.new_page() user_email = generate_random_email() - - page.goto('https://dev.new.expensify.com:8082/') - + page.goto('https://dev.new.expensify.com:8082/') page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() page.wait_for_timeout(2000) - - try: + try: page.locator('button[tabindex="0"]').click() page.wait_for_timeout(2000) except Exception: @@ -44,7 +41,6 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("button", name="Continue").last.click() @@ -55,16 +51,15 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - def test_iou_subtitle_offline(): with sync_playwright() as p: - browser, page, context, user_email = login_user(p) + browser, page, context, user_email = login_user(p) + # Start tracing context.tracing.start(screenshots=True, snapshots=True, sources=True) - + try: - context.set_offline(True) page.wait_for_timeout(2000) - page.get_by_label("Start chat (Floating action)").click() page.get_by_text("Submit expense").click() page.get_by_label("Manual").click() @@ -72,24 +67,25 @@ def test_iou_subtitle_offline(): page.locator("#numPadContainerView").get_by_role("button", name="Next").click() page.get_by_test_id("selection-list-text-input").fill("testuser@gmail.com") page.get_by_label("testuser@gmail.com").click() - + save_button = page.locator('button[data-listener="Enter"]', has_text="Submit") save_button.click() page.wait_for_timeout(1000) - page.get_by_label("View details").click() + expect(page.get_by_label("From testuser@gmail.com")).to_be_visible() + page.get_by_label("From testuser@gmail.com").click() expect(page.get_by_text("This is the beginning of your chat with testuser@gmail.com.")).to_be_visible() - except Exception as e: - - raise e finally: + # Stop tracing and export the trace + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) + trace_cleaner(trace_path) + browser.close() - context.tracing.stop() - context.tracing.export(path="/app/expensify/user_tool/output_browser1.zip") - trace_cleaner("/app/expensify/user_tool/output_browser1.zip") - context.close() - browser.close() +# If you want to run the test directly +if __name__ == '__main__': + test_iou_subtitle_offline() \ No newline at end of file diff --git a/issues/26228_719/user_tool.py b/issues/26228_719/user_tool.py index e2d2975..39978fb 100644 --- a/issues/26228_719/user_tool.py +++ b/issues/26228_719/user_tool.py @@ -7,31 +7,31 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from utils.trace_cleaner import trace_cleaner + def get_user_email(): return f"nitish.expensify+{9}@gmail.com" + def get_magic_code(): return "123456" + def login_user(p: PlaywrightContextManager): - browser = p.chromium.launch(headless=True, - args=[ - "--ignore-certificate-errors", - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ], - slow_mo=1000) + browser = p.chromium.launch(headless=True, args=[ + "--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + slow_mo=1000) + + # Create a new context and start tracing context = browser.new_context() + context.tracing.start(screenshots=True, snapshots=True, sources=True) page = context.new_page() user_email = get_user_email() first_name = 'John' last_name = 'Doe' - page.goto('https://dev.new.expensify.com:8082/') - page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() @@ -42,14 +42,8 @@ def login_user(p: PlaywrightContextManager): page.get_by_role("button", name="Join").click() try: - expect(page.locator('div[aria-label="Track and budget expenses"]').nth( - 0)).to_be_visible() - - - page.locator( - 'div[aria-label="Track and budget expenses"]').nth(0).click() - - + expect(page.locator('div[aria-label="Track and budget expenses"]').nth(0)).to_be_visible() + page.locator('div[aria-label="Track and budget expenses"]').nth(0).click() page.locator('input[name="fname"]').nth(0).fill(first_name) page.locator('input[name="lname"]').nth(0).fill(last_name) page.get_by_role("button", name="Continue").nth(0).click() @@ -57,8 +51,7 @@ def login_user(p: PlaywrightContextManager): pass try: - get_started_here = page.locator( - 'div', has_text="Get started here!").nth(-1) + get_started_here = page.locator('div', has_text="Get started here!").nth(-1) expect(get_started_here).to_be_visible() page.get_by_label("Close").nth(-1).click() except Exception: @@ -70,11 +63,8 @@ def login_user(p: PlaywrightContextManager): def test_26228(): with sync_playwright() as p: browser, context, page = login_user(p) - - context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - plus_icon = page.locator( - 'button[aria-label="Start chat (Floating action)"]') + plus_icon = page.locator('button[aria-label="Start chat (Floating action)"]') expect(plus_icon).to_be_visible() plus_icon.click() @@ -84,15 +74,8 @@ def test_26228(): inputBox = page.get_by_test_id("selection-list-text-input") expect(inputBox).to_be_visible() - except Exception as e: - print("Test encountered an error:", e) - raise e finally: - - context.tracing.stop() - context.tracing.export("/app/expensify/user_tool/output_browser1.zip") - - - trace_cleaner("/app/expensify/user_tool/output_browser1.zip") - + trace_file = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_file) + trace_cleaner(trace_file) browser.close() diff --git a/issues/26648_972/user_tool.py b/issues/26648_972/user_tool.py index 162e537..743fff8 100644 --- a/issues/26648_972/user_tool.py +++ b/issues/26648_972/user_tool.py @@ -18,11 +18,14 @@ import re import random +from utils.trace_cleaner import trace_cleaner + NEWDOT_URL = "https://dev.new.expensify.com:8082/" OLDDOT_URL = "http://localhost:9000/" EMAIL = "turingvidhant+111@gmail.com" PASSWORD = "uotx fzkt kfeg dehz" + class IMAPOTPExtractor: def __init__(self, email_address, password): if not self._validate_email(email_address): @@ -50,7 +53,7 @@ def check_for_otp(self): if status != "OK": return None - email_ids = messages[0].split() + email_ids = messages[0].split() email_ids = email_ids[::-1] for email_id in email_ids: @@ -97,6 +100,7 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) @@ -114,6 +118,7 @@ def new_dot_login(page, email): except: pass + def launch_browser(pw, headless=True, device=None, geolocation=None): """ Launch the browser. @@ -138,22 +143,15 @@ def test_expensify(): ]) first_user_context = browser.new_context(ignore_https_errors=True) + # Start tracing with screenshots, snapshots and sources + first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) + page = first_user_context.new_page() try: - - first_user_context.tracing.start( - title="test_expensify", - screenshots=True, - snapshots=True, - sources=True - ) - - page = first_user_context.new_page() page.wait_for_timeout(3000) - new_dot_login(page, EMAIL) page.wait_for_timeout(3000) - + page.get_by_text("This is your personal space. Use it for notes, tasks, drafts, and reminders.").click() page.get_by_label("Start chat (Floating action)").click() @@ -161,7 +159,6 @@ def test_expensify(): page.get_by_label("Manual").click() scan_button = page.get_by_label("Scan") - distance_button = page.get_by_label("Distance") distance_initial_bg_color = distance_button.evaluate("element => window.getComputedStyle(element).backgroundColor") @@ -180,18 +177,9 @@ def test_expensify(): assert distance_initial_bg_color != distance_hover_bg_color, "Distance Button Background color did not change on hover" page.wait_for_timeout(1500) - except Exception as e: - - raise e finally: - - first_user_context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(path=trace_path) - - - from utils.trace_cleaner import trace_cleaner + first_user_context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - page.close() browser.close() diff --git a/issues/27156_665/user_tool.py b/issues/27156_665/user_tool.py index e7b771a..c279c72 100644 --- a/issues/27156_665/user_tool.py +++ b/issues/27156_665/user_tool.py @@ -17,7 +17,6 @@ import email import re import random - from utils.trace_cleaner import trace_cleaner NEWDOT_URL = "https://dev.new.expensify.com:8082/" @@ -25,6 +24,7 @@ EMAIL = "turingvidhant+114@gmail.com" PASSWORD = "uotx fzkt kfeg dehz" + class IMAPOTPExtractor: def __init__(self, email_address, password): if not self._validate_email(email_address): @@ -99,11 +99,13 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) + def new_dot_login(page, email): - page.goto(NEWDOT_URL) + page.goto(NEWDOT_URL) page.locator('input[type="email"]').fill(email) page.wait_for_timeout(3000) page.get_by_role("button", name="Continue").nth(0).click() @@ -134,22 +136,16 @@ def launch_browser(pw, headless=True, device=None, geolocation=None): def test_expensify(): - error = None - with sync_playwright() as p: + with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=[ "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" ]) - context = browser.new_context(ignore_https_errors=True) - context.tracing.start( - title="test-expensify-trace", - screenshots=True, - snapshots=True, - sources=True - ) - - page = context.new_page() + first_user_context = browser.new_context(ignore_https_errors=True) + # Start tracing immediately after creating the context + first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) + page = first_user_context.new_page() page.wait_for_timeout(3000) try: @@ -157,13 +153,11 @@ def test_expensify(): page.wait_for_timeout(3000) amount = "100" - workspace_chat = page.locator('button[aria-label="Navigates to a chat"]', has_text="Turing Vidhant+114's Workspace") expect(workspace_chat).to_be_visible() workspace_chat.click() page.wait_for_timeout(2000) - plus_create_icon = page.locator('button[aria-label="Create"]').last expect(plus_create_icon).to_be_visible() plus_create_icon.click() @@ -174,21 +168,18 @@ def test_expensify(): submit_expense_button.click() page.wait_for_timeout(2000) - - manual_button = page.locator('button[aria-label="Manual"]') + manual_button = page.locator('button[aria-label="Manual"]'). expect(manual_button).to_be_visible() manual_button.click() page.wait_for_timeout(2000) page.locator('input[role="presentation"]').fill(amount) - next_button = page.locator('button[data-listener="Enter"]', has_text="Next").first expect(next_button).to_be_visible() next_button.click() page.wait_for_timeout(2000) - merchant_field = page.locator('div[role="menuitem"]', has_text="Merchant") expect(merchant_field).to_be_visible() merchant_field.click() @@ -201,46 +192,41 @@ def test_expensify(): save_button.click() page.wait_for_timeout(2000) - save_button = page.locator('button[data-listener="Enter"]', has_text="Submit") expect(save_button).to_be_visible() save_button.click() page.wait_for_timeout(2000) - new_workspace_chat = page.locator('button[aria-label="Navigates to a chat"]', has_text="Turing Vidhant+114's Workspace") expect(new_workspace_chat).to_be_visible() new_workspace_chat.click() page.wait_for_timeout(2000) - + view_details = page.locator('button[aria-label="View details"]').last expect(view_details).to_be_visible() view_details.click() - - + page.get_by_role("textbox").type(':eye:', delay=100) page.wait_for_timeout(2000) - - + smile_emoji = page.locator('div[aria-label="smile+1}"]') page.wait_for_timeout(2000) - + if smile_emoji.is_visible(): assert False, "Emoji suggestion does not auto close after inserting emoji." else: assert True, "Emoji suggestion auto close after inserting emoji." - + page.wait_for_timeout(1500) - except Exception as e: - error = e - finally: - context.tracing.stop() + finally: + # Ensure that tracing is stopped and the trace file is exported trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) + first_user_context.tracing.stop(path=trace_path) trace_cleaner(trace_path) page.close() browser.close() - if error: - raise error + +if __name__ == '__main__': + test_expensify() diff --git a/issues/28021_804/user_tool.py b/issues/28021_804/user_tool.py index ca13d12..31e9f95 100644 --- a/issues/28021_804/user_tool.py +++ b/issues/28021_804/user_tool.py @@ -1,12 +1,14 @@ import sys, os +from utils.trace_cleaner import trace_cleaner + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) import os + import requests # pip install requests from playwright.sync_api import Page, TimeoutError, expect, sync_playwright -from utils.trace_cleaner import trace_cleaner EMAIL_USERNAME = "xecuycismfsga" EMAIL_ALIAS = "um9vc7" @@ -24,7 +26,6 @@ def download_image( script_dir = os.path.dirname(__file__) file_path = os.path.join(script_dir, cache_dir, filename) - if not os.path.exists(file_path): response = requests.get(url) response.raise_for_status() # Added for better error handling @@ -48,7 +49,6 @@ def get_test_image(url: str, cache_dir="test_assets", filename="downloaded_image def create_user(page: Page, firstname: str = "User", lastname: str = EMAIL_ALIAS): page.get_by_role("button", name="Join").click() - page.get_by_text("Track and budget expenses").click() page.get_by_role("button", name="Continue").last.click() page.get_by_role("textbox", name="First name").fill(firstname) @@ -89,7 +89,6 @@ def login_or_create_user( def test_replace_receipt_scan(): with sync_playwright() as p: - browser = p.chromium.launch( headless=True, slow_mo=SLOW_MO, @@ -100,19 +99,15 @@ def test_replace_receipt_scan(): ], ) context = browser.new_context() - - + # Start tracing for the context context.tracing.start(screenshots=True, snapshots=True, sources=True) - trace_path = "/app/expensify/user_tool/output_browser1.zip" + page = context.new_page() try: - page = context.new_page() page.goto(NEW_DOT_URL) - login_or_create_user(page) - chat = page.locator( f'button[aria-label="Navigates to a chat"]:has-text("{PARTICIPANT_EMAIL}")' ).first @@ -120,19 +115,13 @@ def test_replace_receipt_scan(): try: chat.click(timeout=3000) except TimeoutError: - page.get_by_label("Start chat (Floating action)").click() page.get_by_label("Start chat", exact=True).click() page.get_by_test_id("selection-list-text-input").fill(PARTICIPANT_EMAIL) - page.get_by_test_id("selection-list").get_by_label( - PARTICIPANT_EMAIL - ).click() - - + page.get_by_test_id("selection-list").get_by_label(PARTICIPANT_EMAIL).click() page.get_by_role("textbox").fill("Hey") page.keyboard.press("Enter") - page.get_by_role("button", name="Create").click() page.get_by_label("Submit expense").click() page.get_by_label("Scan").click() @@ -146,7 +135,6 @@ def test_replace_receipt_scan(): page.get_by_role("button", name="Submit expense").click() page.get_by_role("button", name="Not now").click() - page.get_by_role("button", name="View details").last.click() page.get_by_text("Receipt scanning...").last.click() page.get_by_label("View attachment").last.click() @@ -158,12 +146,14 @@ def test_replace_receipt_scan(): expect(page.get_by_text("Replace")).not_to_be_visible() finally: - - context.tracing.stop() - context.tracing.export(path=trace_path) - - + # Stop the tracing session and export the trace file + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - context.close() browser.close() + + +# If running as a script +if __name__ == "__main__": + test_replace_receipt_scan() \ No newline at end of file diff --git a/issues/28077_782/user_tool.py b/issues/28077_782/user_tool.py index 90f2feb..725214d 100644 --- a/issues/28077_782/user_tool.py +++ b/issues/28077_782/user_tool.py @@ -13,8 +13,8 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page -from utils.trace_cleaner import trace_cleaner +from utils.trace_cleaner import trace_cleaner def generate_random_email(base_email="testerbro7077@gmail.com"): @@ -23,7 +23,6 @@ def generate_random_email(base_email="testerbro7077@gmail.com"): return f"{email_user}+{timestamp}@{domain}" - def get_magic_code(user_email, password, retries=5, delay=10): imap = imaplib.IMAP4_SSL("imap.gmail.com") imap.login(user_email, password) @@ -62,7 +61,6 @@ def get_magic_code(user_email, password, retries=5, delay=10): return None - def login_user(p: PlaywrightContextManager, first_name="Yagna", last_name="Sai") -> tuple[Browser, Page, str]: user_email = generate_random_email() password = "xqxz mmcb tvkn lpgp" @@ -85,7 +83,6 @@ def login_user(p: PlaywrightContextManager, first_name="Yagna", last_name="Sai") page.wait_for_timeout(1000) - magic_code = "123456" validate_code_input = page.locator('input[data-testid="validateCode"]') @@ -133,7 +130,6 @@ def login_user2(p: PlaywrightContextManager, first_name="Yagna", last_name="Sai" page.wait_for_timeout(1000) - magic_code = "123456" validate_code_input = page.locator('input[data-testid="validateCode"]') @@ -167,42 +163,28 @@ def check_name2(page): @pytest.mark.parametrize("first_name, last_name", [("Yagna", "Sai")]) def test_run(first_name: str, last_name: str) -> None: with sync_playwright() as playwright: - - trace_path1 = "/app/expensify/user_tool/output_browser1.zip" - browser1 = page1 = context1 = None + # First browser session with tracing try: browser1, page1, user_email1 = login_user(playwright, first_name, last_name) context1 = page1.context - context1.tracing.start(title="login_user", screenshots=True, snapshots=True, sources=True) + context1.tracing.start(screenshots=True, snapshots=True, sources=True) print(f"Logged in with email: {user_email1}") check_name(page1) - except Exception as e: - print(f"Error in first part: {e}") finally: - if context1: - context1.tracing.stop() - context1.tracing.export(path=trace_path1) - trace_cleaner(trace_path1) - if browser1: - browser1.close() - + context1.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + browser1.close() - trace_path2 = "/app/expensify/user_tool/output_browser2.zip" - browser2 = page2 = context2 = None + # Second browser session with tracing try: browser2, page2, user_email2 = login_user2(playwright, first_name, last_name) context2 = page2.context - context2.tracing.start(title="login_user2", screenshots=True, snapshots=True, sources=True) + context2.tracing.start(screenshots=True, snapshots=True, sources=True) check_name2(page2) - except Exception as e: - print(f"Error in second part: {e}") finally: - if context2: - context2.tracing.stop() - context2.tracing.export(path=trace_path2) - trace_cleaner(trace_path2) - if browser2: - browser2.close() + context2.tracing.stop(path="/app/expensify/user_tool/output_browser2.zip") + trace_cleaner("/app/expensify/user_tool/output_browser2.zip") + browser2.close() if __name__ == "__main__": diff --git a/issues/28087_800/user_tool.py b/issues/28087_800/user_tool.py index 40f7f2e..6df9678 100644 --- a/issues/28087_800/user_tool.py +++ b/issues/28087_800/user_tool.py @@ -4,22 +4,28 @@ sys.path.insert(0, base_path) from playwright.sync_api import sync_playwright, expect +from playwright.sync_api._context_manager import PlaywrightContextManager from utils.trace_cleaner import trace_cleaner + def get_user_email(): return f"nitish.expensify+{84}@gmail.com" + def get_magic_code(): return "123456" -def login_user(context, first_name="Milan", last_name="T"): + +def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T"): + browser = p.chromium.launch(headless=True, args=[ + "--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + slow_mo=2000) + context = browser.new_context() page = context.new_page() user_email = get_user_email() - page.goto('https://dev.new.expensify.com:8082/') - page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() @@ -30,13 +36,8 @@ def login_user(context, first_name="Milan", last_name="T"): page.get_by_role("button", name="Join").click() try: - expect(page.locator('div[aria-label="Track and budget expenses"]').nth( - 0)).to_be_visible() - - + expect(page.locator('div[aria-label="Track and budget expenses"]').nth(0)).to_be_visible() page.locator('div[aria-label="Track and budget expenses"]').nth(0).click() - - page.locator('input[name="fname"]').nth(0).fill(first_name) page.locator('input[name="lname"]').nth(0).fill(last_name) page.get_by_role("button", name="Continue").nth(0).click() @@ -44,29 +45,22 @@ def login_user(context, first_name="Milan", last_name="T"): pass try: - get_started_here = page.locator( - 'div', has_text="Get started here!").nth(-1) + get_started_here = page.locator('div', has_text="Get started here!").nth(-1) expect(get_started_here).to_be_visible() page.get_by_label("Close").nth(-1).click() except Exception: pass - return page + return page, browser + def test_save_description_in_submitted_expense(): with sync_playwright() as p: - browser = p.chromium.launch(headless=True, args=[ - "--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], - slow_mo=2000 - ) - context = browser.new_context() - context.tracing.start(screenshots=True, snapshots=True, sources=True) - + page, browser = login_user(p) + # Start tracing on the browser context + page.context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - page = login_user(context) - - page.locator( - 'button[aria-label="Start chat (Floating action)"]').last.click() + page.locator('button[aria-label="Start chat (Floating action)"]').last.click() page.locator('div', has_text="New Workspace").last.click() page.locator('button[aria-label="Back"]').last.click() page.locator('button[aria-label="Inbox"]').last.click() @@ -80,12 +74,13 @@ def test_save_description_in_submitted_expense(): page.locator('div[aria-label="Leave"]').last.click() side_bar = page.locator('div[data-testid="BaseSidebarScreen"]').last expect(side_bar).to_be_visible() - except Exception as e: - raise e finally: - context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) + # Stop tracing, export and clean the trace regardless of test outcome + page.context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - context.close() browser.close() + + +if __name__ == "__main__": + test_save_description_in_submitted_expense() \ No newline at end of file diff --git a/issues/28092_839/user_tool.py b/issues/28092_839/user_tool.py index d34002e..ef34708 100644 --- a/issues/28092_839/user_tool.py +++ b/issues/28092_839/user_tool.py @@ -186,8 +186,8 @@ async def test_recovery_code_login(): raise e finally: - await context.tracing.stop() - await context.tracing.export(path="/app/expensify/user_tool/output_browser1.zip") + + await context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") trace_cleaner("/app/expensify/user_tool/output_browser1.zip") diff --git a/issues/28943_641/user_tool.py b/issues/28943_641/user_tool.py index 5d3eebf..ede2156 100644 --- a/issues/28943_641/user_tool.py +++ b/issues/28943_641/user_tool.py @@ -221,24 +221,19 @@ def submit_invalid_expense(page): @pytest.mark.parametrize("first_name, last_name", [("Test", "User")]) def test_selected_highlight(setup_playwright, first_name, last_name): context = setup_playwright - - + # Start tracing for this context context.tracing.start(screenshots=True, snapshots=True, sources=True) try: account = EmailHandler(email_address=BASE_EMAIL, password=PASSWORD) user_email = account.get_email_address() - page = context.new_page() login_user(page, account) - workspace_name = create_a_new_workspace(page) - access_target_workspace_chat(page, workspace_name) - page.get_by_label("Test User's Workspace").click() page.get_by_test_id("ReportDetailsPage").get_by_text("Settings").click() @@ -246,11 +241,9 @@ def test_selected_highlight(setup_playwright, first_name, last_name): page.get_by_text("Notify me about new messages").click() selected_option = page.get_by_test_id("selection-list").get_by_label("Immediately") expect(selected_option).to_have_css("background-color", "rgba(0, 0, 0, 0)") - except Exception as e: - raise e finally: - - context.tracing.stop() - context.tracing.export("/app/expensify/user_tool/output_browser1.zip") - - trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) + trace_cleaner(trace_path) + # Closing the context if not already closed by the fixture + context.close() diff --git a/issues/29163_589/user_tool.py b/issues/29163_589/user_tool.py index 47dd348..26872c8 100644 --- a/issues/29163_589/user_tool.py +++ b/issues/29163_589/user_tool.py @@ -195,6 +195,6 @@ async def test_tags_disabled_button(browser_context): except Exception as e: raise e finally: - await context.tracing.stop() - await context.tracing.export(trace_path) + + await context.tracing.stop(path=trace_path) trace_cleaner(trace_path) diff --git a/issues/29222_720/user_tool.py b/issues/29222_720/user_tool.py index bba9729..c643a02 100644 --- a/issues/29222_720/user_tool.py +++ b/issues/29222_720/user_tool.py @@ -17,6 +17,7 @@ import email import re import random + from utils.trace_cleaner import trace_cleaner NEWDOT_URL = "https://dev.new.expensify.com:8082/" @@ -24,6 +25,7 @@ EMAIL = "turingvidhant+109@gmail.com" PASSWORD = "uotx fzkt kfeg dehz" + class IMAPOTPExtractor: def __init__(self, email_address, password): if not self._validate_email(email_address): @@ -98,6 +100,7 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) @@ -115,6 +118,7 @@ def new_dot_login(page, email): except: pass + def launch_browser(pw, headless=True, device=None, geolocation=None): """ Launch the browser. @@ -132,17 +136,19 @@ def launch_browser(pw, headless=True, device=None, geolocation=None): def test_expensify(): - with sync_playwright() as p: + with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=[ "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" ]) first_user_context = browser.new_context(ignore_https_errors=True) + # Start tracing for this context first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) - page = first_user_context.new_page() try: + page = first_user_context.new_page() page.wait_for_timeout(3000) + new_dot_login(page, EMAIL) page.wait_for_timeout(3000) @@ -172,14 +178,20 @@ def test_expensify(): expect(page.get_by_text("India Gate", exact=True)).to_be_visible() expect(page.get_by_text("Colorado Springs", exact=True)).to_be_visible() + page.wait_for_timeout(1500) - except Exception as e: - raise e finally: - first_user_context.tracing.stop() + # Stop tracing and export the trace file trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(path=trace_path) + first_user_context.tracing.stop(path=trace_path) trace_cleaner(trace_path) + try: + page.close() + except Exception: + pass + first_user_context.close() + browser.close() + - page.close() - browser.close() \ No newline at end of file +if __name__ == "__main__": + test_expensify() diff --git a/issues/29598_896/user_tool.py b/issues/29598_896/user_tool.py index a48ca00..51fd20f 100644 --- a/issues/29598_896/user_tool.py +++ b/issues/29598_896/user_tool.py @@ -3,15 +3,13 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner from playwright.sync_api import sync_playwright, Locator from playwright._impl._page import Page import os from PIL import Image -from utils.trace_cleaner import trace_cleaner - - def generate_random_email(username: str = "expensifyapp97"): return f"{username}-19876543112@gmail.com".lower() @@ -20,14 +18,11 @@ def login_user(page: Page, first_name="Milan", last_name="T"): user_email = generate_random_email(f"{first_name}-{last_name}") - page.goto('https://dev.new.expensify.com:8082/') - page.locator('input[type="email"]').fill(user_email) page.locator('button', has_text="Continue").click() - page.locator('button', has_text="Join").click() is_name_set = True @@ -36,7 +31,6 @@ def login_user(page: Page, first_name="Milan", last_name="T"): page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("form").get_by_role("button", name="Continue").click() @@ -46,6 +40,7 @@ def login_user(page: Page, first_name="Milan", last_name="T"): return user_email, is_name_set + def check_if_an_element_is_entirely_clickable(page: Page, element: Locator): """ This function checks if an element is entirely clickable. @@ -53,17 +48,15 @@ def check_if_an_element_is_entirely_clickable(page: Page, element: Locator): element.wait_for() - element_box = element.bounding_box() - rows, cols = 3, 3 for i in range(rows): for j in range(cols): x = element_box['x'] + (j + 0.5) * element_box['width'] / cols y = element_box['y'] + (i + 0.5) * element_box['height'] / rows - + page.mouse.move(x, y) is_clickable = page.evaluate(""" ([x, y]) => { const el = document.elementFromPoint(x, y); @@ -74,40 +67,34 @@ def check_if_an_element_is_entirely_clickable(page: Page, element: Locator): return False return True + def check_if_close_button_is_entirely_clickable(page: Page, file_name: str): """ This function checks if the attachment close button is entirely clickable. """ - current_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(current_dir, file_name) - page.get_by_text("Asaad I (you)").wait_for() page.get_by_text("Asaad I (you)").click() - page.get_by_test_id("report-actions-list").get_by_text("Your space").wait_for() page.get_by_label("Create").last.click() - with page.expect_file_chooser() as fc_info: page.get_by_label("Add attachment").click() - file_chooser = fc_info.value file_chooser.set_files(file_path) - - close_button = page.get_by_label("Close") - is_clickable = check_if_an_element_is_entirely_clickable(page, close_button) assert is_clickable, f"The Close button is not entirely clickable" + def create_test_image(file_path: str): """ Creates a simple PNG image and saves it at the specified path. @@ -115,6 +102,7 @@ def create_test_image(file_path: str): img = Image.new('RGB', (100, 100), color=(73, 109, 137)) img.save(file_path) + def test_close_button_is_entirely_clickable(): """ Test to verify that the close button is entirely clickable @@ -127,19 +115,14 @@ def test_close_button_is_entirely_clickable(): args = ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"] browser = p.chromium.launch(headless=True, args=args, proxy=proxy) context = browser.new_context() + page = context.new_page() - + # Start tracing context.tracing.start(screenshots=True, snapshots=True, sources=True) - trace_path = "/app/expensify/user_tool/output_browser1.zip" - error = None try: - page = context.new_page() - - user_email, is_name = login_user(page, first_name, last_name) - if not is_name: page.locator('button[aria-label="My settings"]').click() page.get_by_label(user_email).first.click() @@ -149,22 +132,19 @@ def test_close_button_is_entirely_clickable(): page.get_by_role("textbox", name="Last name").fill(last_name) page.get_by_role("button", name="Save").click() page.locator('button[aria-label="Inbox"]').click() - + home_directory = os.path.expanduser("~") test_image_path = os.path.join(home_directory, "test_image_upload.png") create_test_image(test_image_path) - check_if_close_button_is_entirely_clickable(page, test_image_path) - except Exception as e: - error = e finally: - - context.tracing.stop() - context.tracing.export(trace_path) - + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) browser.close() - if error: - raise error + +# You can call the test function to run the test +if __name__ == '__main__': + test_close_button_is_entirely_clickable() \ No newline at end of file diff --git a/issues/29854_602/user_tool.py b/issues/29854_602/user_tool.py index 47ebd1b..345b313 100644 --- a/issues/29854_602/user_tool.py +++ b/issues/29854_602/user_tool.py @@ -22,61 +22,59 @@ EXPENSIFY_PASSWORD = "kkfr hqiv yuxq rbso" - logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s") LOGGER = logging.getLogger(__name__) def create_dummy_image(): - - img = Image.new('RGB', (100, 100), color = 'red') + img = Image.new('RGB', (100, 100), color='red') byte_io = BytesIO() img.save(byte_io, 'PNG') byte_io.seek(0) return byte_io - - TESTING_FLOW = True + @pytest_asyncio.fixture async def page(): """ Launch the Expensify app. """ pw = await async_playwright().start() - browser = await pw.chromium.launch(channel="chrome", headless=True, args=["--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" ], proxy={"server": "http://localhost:8080"}, slow_mo=500) + browser = await pw.chromium.launch(channel="chrome", headless=True, args=["--ignore-certificate-errors", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], proxy={"server": "http://localhost:8080"}, slow_mo=500) context = await browser.new_context(ignore_https_errors=True) page = await context.new_page() yield page + + # Original cleanup (if browser is still open, this may be a redundant close if already closed in test) await browser.close() await pw.stop() + @pytest.mark.asyncio async def test_the_issue(page): """ - Demonstrate the Expensify app email login verification. + Demonstrate the Expensify app email login verification with tracing. """ + await page.goto(NEWDOT_URL) - await page.context.tracing.start(screenshots=True, snapshots=True, sources=True) - try: - await page.goto(NEWDOT_URL) - + # Start tracing using the page context + context = page.context + await context.tracing.start(screenshots=True, snapshots=True, sources=True) + try: with EmailHandler(EXPENSIFY_EMAIL, EXPENSIFY_PASSWORD) as email_handler: - if not TESTING_FLOW : + if not TESTING_FLOW: email_handler.clean_inbox() # Clean inbox - await page.get_by_role("textbox", name="Phone or email").fill(EXPENSIFY_EMAIL) await page.get_by_role("button", name="Continue").click() - otp = email_handler.read_otp_code() if not TESTING_FLOW else "123456" await page.locator('input[name="validateCode"]').first.fill(otp) - await page.get_by_text("Please enter the magic code").wait_for(state="hidden") logging.info("Sign in complete.") @@ -89,29 +87,24 @@ async def test_the_issue(page): await page.locator('div[aria-label="test1@gmail.com"]').last.click() await page.locator('div[aria-label="More"]').last.click() await page.locator('text="Add receipt"').last.click() - - + dummy_image = create_dummy_image() async with page.expect_file_chooser() as fc_info: await page.locator('text="Choose file"').last.click() file_chooser = await fc_info.value - - await file_chooser.set_files([{ + await file_chooser.set_files([{ "name": "dummy_image.png", "mimeType": "image/png", "buffer": dummy_image.getvalue() }]) two_100_dollar = page.get_by_role("button", name="Request").filter(has_text="200.0") - - + await expect(two_100_dollar).to_be_visible() - - except Exception as e: - LOGGER.error(f"Test failed with error: {e}") - raise e finally: - - await page.context.tracing.stop() - await page.context.tracing.export(path="/app/expensify/user_tool/output_browser1.zip") - - trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + # Stop tracing and export the trace file + trace_path = "/app/expensify/user_tool/output_browser1.zip" + await context.tracing.stop(path=trace_path) + trace_cleaner(trace_path) + + # Close the browser + await page.context.browser.close() \ No newline at end of file diff --git a/issues/30195_650/user_tool.py b/issues/30195_650/user_tool.py index e92bbb9..71a00b5 100644 --- a/issues/30195_650/user_tool.py +++ b/issues/30195_650/user_tool.py @@ -3,24 +3,23 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import time from playwright.sync_api import sync_playwright, Page, expect import re -from utils.trace_cleaner import trace_cleaner - EMAIL = "t1533148+123@gmail.com" def login_user(p): - browser = p.chromium.launch(channel='chrome', headless=True, - proxy = {"server": "http://127.0.0.1:8080/"}, + proxy={"server": "http://127.0.0.1:8080/"}, args=[ - "--ignore-certificate-errors", - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ]) + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process" + ]) context_args = {} context_args["geolocation"] = {"latitude": 37.7749, "longitude": 122.4194} context_args["permissions"] = ["geolocation"] @@ -29,7 +28,6 @@ def login_user(p): page = context.new_page() user_email = EMAIL - page.goto('https://127.0.0.1:8082/') page.locator('input[type="email"]').fill(user_email) @@ -45,17 +43,11 @@ def login_user(p): def test_field_validation(): with sync_playwright() as pw: browser, page, context = login_user(pw) - - context.tracing.start( - title="test_field_validation", - screenshots=True, - snapshots=True, - sources=True - ) + # Start tracing immediately after context creation + context.tracing.start(screenshots=True, snapshots=True, sources=True) try: page.wait_for_timeout(2000) - page.get_by_label("My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").get_by_text("Workspaces").click() page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").click() @@ -66,14 +58,12 @@ def test_field_validation(): page.get_by_label("Workflows").click() page.wait_for_timeout(1000) - page.get_by_label("Connect bank account").click() page.wait_for_timeout(1000) page.get_by_role("button", name="Update to USD").click() page.wait_for_timeout(1000) page.get_by_label("Connect manually").click() - page.get_by_role("textbox", name="Routing number").fill("071004200") page.wait_for_timeout(1000) page.get_by_role("textbox", name="Account number").fill("871698019") @@ -94,7 +84,6 @@ def test_field_validation(): page.get_by_role("button", name="Next").click() page.wait_for_timeout(1000) - page.get_by_label("Street address").get_by_role("textbox").click() page.wait_for_timeout(1000) page.get_by_role("textbox", name="City").click() @@ -104,15 +93,14 @@ def test_field_validation(): page.get_by_test_id("StateSelectionPage").get_by_label("Back").click() page.wait_for_timeout(1000) - expect(page.get_by_test_id("PersonalInfo").get_by_role("menuitem").get_by_text("This field is required.")).to_be_visible() finally: - - context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) - - trace_cleaner(trace_path) - + # Stop tracing and export the trace file + context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") context.close() browser.close() + + +if __name__ == "__main__": + test_field_validation() \ No newline at end of file diff --git a/issues/37688_441/user_tool.py b/issues/37688_441/user_tool.py index 54d1d19..deef6db 100644 --- a/issues/37688_441/user_tool.py +++ b/issues/37688_441/user_tool.py @@ -9,7 +9,6 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page - from utils.trace_cleaner import trace_cleaner def generate_future_date_string(): @@ -19,32 +18,29 @@ def generate_future_date_string(): TIMEOUT = 200000 def login_user(p, first_name="Milan", last_name="T", user_email=None): - browser = p.chromium.launch(headless=True, args=[ - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process", - ], slow_mo=1000) + browser = p.chromium.launch(headless=True, args=[ + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process", + ], slow_mo=1000) + context_args = {} context_args["timezone_id"] = "Asia/Dhaka" + # Create context and start tracing context = browser.new_context() context.set_default_navigation_timeout(TIMEOUT) context.set_default_timeout(TIMEOUT) context.set_default_navigation_timeout(TIMEOUT) - - context.tracing.start(screenshots=True, snapshots=True, sources=True) page = context.new_page() - page.goto("https://dev.new.expensify.com:8082/") - page.locator('input[type="email"]').fill(user_email) page.get_by_role("button", name="Continue").click() - page.get_by_role("button", name="Join").click() try: @@ -52,7 +48,6 @@ def login_user(p, first_name="Milan", last_name="T", user_email=None): something_else_button = page.locator('div[aria-label="Something else"]') something_else_button.click() - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("button", name="Continue").last.click() @@ -64,15 +59,10 @@ def login_user(p, first_name="Milan", last_name="T", user_email=None): def test_banned_user_banner_appears_in_chat_instead_of_textbox(): with sync_playwright() as p: - user_email_1 = "rgarciatammy4+1733074305@gmail.com" - user_email_2 = "rgarciatammy4+1733074306@gmail.com" + # Initialize browser sessions + browser, context, page, email = login_user(p, "User", "One", user_email="rgarciatammy4+1733074305@gmail.com") + browser1, context1, page1, email1 = login_user(p, "User", "Two", user_email="rgarciatammy4+1733074306@gmail.com") - - browser, context, page, email = login_user(p, "User", "One", user_email_1) - - - browser1, context1, page1, email1 = login_user(p, "User", "Two", user_email_2) - try: start_chat_fab = page.get_by_role("button", name="Start chat (Floating action)") start_chat_fab.click() @@ -98,8 +88,8 @@ def test_banned_user_banner_appears_in_chat_instead_of_textbox(): page.evaluate( """async (date) => { - await Onyx.set('nvp_private_blockedFromChat', date); - }""", + await Onyx.set('nvp_private_blockedFromChat', date); + }""", generate_future_date_string(), ) page.wait_for_timeout(3000) @@ -107,21 +97,20 @@ def test_banned_user_banner_appears_in_chat_instead_of_textbox(): report_actions_view = page.get_by_test_id("report-actions-view-wrapper").last expect(report_actions_view).to_contain_text("Note: You've been banned from chatting in this channel") - - except Exception as e: - - raise e finally: - - context.tracing.stop() - trace_path_1 = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path_1) - trace_cleaner(trace_path_1) - - context1.tracing.stop() - trace_path_2 = "/app/expensify/user_tool/output_browser2.zip" - context1.tracing.export(trace_path_2) - trace_cleaner(trace_path_2) - + # Stop tracing for the first browser context and export the trace + trace_path1 = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path1) + trace_cleaner(trace_path1) browser.close() + + # Stop tracing for the second browser context and export the trace + trace_path2 = "/app/expensify/user_tool/output_browser2.zip" + context1.tracing.stop(path=trace_path2) + trace_cleaner(trace_path2) browser1.close() + + +# If you want to run the test directly +if __name__ == '__main__': + test_banned_user_banner_appears_in_chat_instead_of_textbox() diff --git a/issues/37766_765/user_tool.py b/issues/37766_765/user_tool.py index 2136ae0..7cdff92 100644 --- a/issues/37766_765/user_tool.py +++ b/issues/37766_765/user_tool.py @@ -15,10 +15,10 @@ def login_user(p: PlaywrightContextManager, user_email) -> tuple[Browser, Page, browser = p.chromium.launch(channel='chrome', headless=True, slow_mo=500, proxy = {"server": "http://127.0.0.1:8080/"}, args=[ - "--ignore-certificate-errors", - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ]) + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process" + ]) context_args = {} context_args["geolocation"] = {"latitude": 37.7749, "longitude": 122.4194} context_args["permissions"] = ["geolocation"] @@ -27,7 +27,6 @@ def login_user(p: PlaywrightContextManager, user_email) -> tuple[Browser, Page, page = context.new_page() user_email = user_email - page.goto('https://127.0.0.1:8082/') page.locator('input[type="email"]').fill(user_email) @@ -43,50 +42,40 @@ def login_user(p: PlaywrightContextManager, user_email) -> tuple[Browser, Page, def test_disable_category(): with sync_playwright() as p: browser_b, page_b, user_email_b = login_user(p, "t1533148+123@gmail.com") - context_b = page_b.context - - - context_b.tracing.start(screenshots=True, snapshots=True, sources=True) - trace_path = '/app/expensify/user_tool/output_browser1.zip' + # Get the context from the page + context = page_b.context + # Start tracing + context.tracing.start(screenshots=True, snapshots=True, sources=True) try: page_b.wait_for_timeout(2000) - - page_b.get_by_test_id("7823501529575664").click() page_b.wait_for_timeout(1000) page_b.get_by_label("View details").click() - - + expense1 = page_b.get_by_text("Cash").nth(1) expense1.click() page_b.wait_for_timeout(1000) - - + cat = page_b.get_by_text("Category", exact=True) cat.click() page_b.wait_for_timeout(2000) - - + recent_section = page_b.get_by_text("Recent") all_section = page_b.get_by_text("All") search_section = page_b.locator('input[aria-label="Search"]') - + expect(recent_section).not_to_be_visible() expect(all_section).not_to_be_visible() expect(search_section).not_to_be_visible() - + page_b.wait_for_timeout(3000) - - except Exception as e: - - raise e finally: - - context_b.tracing.stop() - context_b.tracing.export(trace_path) - + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) + browser_b.close() - browser_b.close() +if __name__ == '__main__': + test_disable_category() \ No newline at end of file diff --git a/issues/38147_954/user_tool.py b/issues/38147_954/user_tool.py index 4b625b0..4ecc7a5 100644 --- a/issues/38147_954/user_tool.py +++ b/issues/38147_954/user_tool.py @@ -18,7 +18,6 @@ import re import random - from utils.trace_cleaner import trace_cleaner NEWDOT_URL = "https://dev.new.expensify.com:8082/" @@ -26,6 +25,7 @@ EMAIL = "turingvidhant+111@gmail.com" PASSWORD = "uotx fzkt kfeg dehz" + class IMAPOTPExtractor: def __init__(self, email_address, password): if not self._validate_email(email_address): @@ -100,11 +100,12 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) def new_dot_login(page, email): - page.goto(NEWDOT_URL) + page.goto(NEWDOT_URL) page.locator('input[type="email"]').fill(email) page.wait_for_timeout(3000) page.get_by_role("button", name="Continue").nth(0).click() @@ -141,28 +142,22 @@ def test_expensify(): "--disable-features=IsolateOrigins,site-per-process" ]) - first_user_context = browser.new_context(ignore_https_errors=True) - first_user_context.tracing.start( - screenshots=True, - snapshots=True, - sources=True - ) + first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) page = first_user_context.new_page() - try: page.wait_for_timeout(3000) - + new_dot_login(page, EMAIL) page.wait_for_timeout(3000) - + page.get_by_label("My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_text("Profile") page.get_by_text("Display name").click() first_name = page.get_by_role("textbox", name="First name").input_value() - last_name= page.get_by_role("textbox", name="Last name").input_value() - fullname=first_name+" "+last_name + last_name = page.get_by_role("textbox", name="Last name").input_value() + fullname = first_name + " " + last_name page.get_by_label("Back").click() page.wait_for_timeout(1000) expect(page.get_by_label("My settings")).to_be_visible() @@ -178,24 +173,13 @@ def test_expensify(): expect(page.get_by_test_id("selection-list-text-input")).to_be_visible() page.get_by_test_id("selection-list-text-input").click() page.get_by_test_id("selection-list-text-input").fill(fullname) - name_to_search=fullname+ " (you)" + name_to_search = fullname + " (you)" expect(page.get_by_label(name_to_search)).to_be_visible() - + page.wait_for_timeout(1500) - except Exception as e: - - first_user_context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(path=trace_path) - trace_cleaner(trace_path) - - raise e - else: - - first_user_context.tracing.stop() + page.close() + finally: trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(path=trace_path) + first_user_context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - finally: - page.close() browser.close() diff --git a/issues/38163_970/user_tool.py b/issues/38163_970/user_tool.py index 2d7e660..e214590 100644 --- a/issues/38163_970/user_tool.py +++ b/issues/38163_970/user_tool.py @@ -1,8 +1,11 @@ import sys, os +from utils.trace_cleaner import trace_cleaner + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) + import time from playwright.sync_api import sync_playwright, expect from playwright.sync_api._context_manager import PlaywrightContextManager @@ -10,8 +13,6 @@ from playwright._impl._page import Page from playwright._impl._browser_context import BrowserContext -from utils.trace_cleaner import trace_cleaner - def test_anonymous_user(): with sync_playwright() as p: @@ -20,17 +21,25 @@ def test_anonymous_user(): "--disable-features=IsolateOrigins,site-per-process" ]) context = browser.new_context() + # Start tracing with screenshots, snapshots, and sources enabled context.tracing.start(screenshots=True, snapshots=True, sources=True) page = context.new_page() + try: page.goto('https://dev.new.expensify.com:8082/r/3295647304331553') - + page.get_by_text('https://dev.new.expensify.com:8082/settings/security').click() - + enable_2fa = page.get_by_text("Enable two-factor authentication to keep your account safe.") expect(enable_2fa).not_to_be_visible() finally: + # Stop tracing and export the trace file context.tracing.stop() - trace_path = '/app/expensify/user_tool/output_browser1.zip' - context.tracing.export(trace_path) + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.export_trace(trace_path) + + # Clean the exported trace file trace_cleaner(trace_path) + + # Close the browser + browser.close() \ No newline at end of file diff --git a/issues/39614_1101/user_tool.py b/issues/39614_1101/user_tool.py index 0269578..dd3ab72 100644 --- a/issues/39614_1101/user_tool.py +++ b/issues/39614_1101/user_tool.py @@ -18,13 +18,14 @@ import re import random -from utils.trace_cleaner import trace_cleaner # <--- Added import +from utils.trace_cleaner import trace_cleaner NEWDOT_URL = "https://dev.new.expensify.com:8082/" OLDDOT_URL = "http://localhost:9000/" EMAIL = "turingvidhant+109@gmail.com" PASSWORD = "uotx fzkt kfeg dehz" + class IMAPOTPExtractor: def __init__(self, email_address, password): if not self._validate_email(email_address): @@ -99,6 +100,7 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) def create_workspace_and_enable_feature(page: Page, feature: str): @@ -259,19 +261,18 @@ def add_report_field_list_value( def test_expensify(): - with sync_playwright() as p: + with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=[ "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" ]) - first_user_context = browser.new_context(ignore_https_errors=True) - + first_user_context = browser.new_context(ignore_https_errors=True) + # Start tracing immediately after context creation first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) - page = first_user_context.new_page() page.wait_for_timeout(3000) - + try: new_dot_login(page, EMAIL) page.wait_for_timeout(3000) @@ -283,18 +284,15 @@ def test_expensify(): page.get_by_label("New workspace").click() page.wait_for_timeout(500) - features_text_element = page.locator('div[dir="auto"]', has_text="More features") features_text_element.click() - documement_button = page.get_by_label("Document and reclaim eligible") documement_button.click() - + taxes_button = page.get_by_test_id("WorkspaceInitialPage").get_by_label("Taxes") taxes_button.click() - add_rate = page.get_by_role("button", name="Add rate") add_rate.click() @@ -304,28 +302,27 @@ def test_expensify(): save_button = page.get_by_test_id("TextSelectorModal").get_by_role("button", name="Save") save_button.click() - + tax_value_button = page.get_by_text("Value") tax_value_button.click() page.get_by_placeholder("0").fill("1") save_button = page.get_by_test_id("AmountSelectorModal").get_by_role("button", name="Save") save_button.click() - + save_button = page.get_by_role("button", name="Save").last save_button.click() page.locator("button").filter(has_text="/1%").click() - element = page.locator('div[dir="auto"]', has_text="Hmm... it's not here") expect(element).not_to_be_visible() - + page.wait_for_timeout(1500) finally: - - first_user_context.tracing.stop() + # Ensure tracing is stopped and exported even on errors trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(trace_path) + first_user_context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - - page.close() browser.close() + +if __name__ == "__main__": + test_expensify() \ No newline at end of file diff --git a/issues/41529_679/user_tool.py b/issues/41529_679/user_tool.py index 3ac717b..453c342 100644 --- a/issues/41529_679/user_tool.py +++ b/issues/41529_679/user_tool.py @@ -104,7 +104,7 @@ async def test_distance_rate_and_workflow_graying_out(): finally: - await context.tracing.stop() - await context.tracing.export(path="/app/expensify/user_tool/output_browser1.zip") + + await context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") trace_cleaner("/app/expensify/user_tool/output_browser1.zip") await close_browser(context, page, playwright) diff --git a/issues/41613_529/user_tool.py b/issues/41613_529/user_tool.py index 1805e66..7ce0ac7 100644 --- a/issues/41613_529/user_tool.py +++ b/issues/41613_529/user_tool.py @@ -6,9 +6,8 @@ from playwright.sync_api import Page, expect, sync_playwright from utils.trace_cleaner import trace_cleaner -def create_user( - page: Page, firstname: str, lastname: str, email: str -): + +def create_user(page: Page, firstname: str, lastname: str, email: str): page.get_by_test_id("username").fill(email) page.wait_for_timeout(1000) @@ -26,15 +25,14 @@ def create_user( def test_task_preview(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True, slow_mo = 1000, - proxy = {"server": "http://127.0.0.1:8080/"}, - args=[ - "--ignore-certificate-errors", - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ]) + browser = p.chromium.launch(headless=True, slow_mo=1000, + proxy={"server": "http://127.0.0.1:8080/"}, + args=[ + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process" + ]) context_args = {} context_args["geolocation"] = {"latitude": 37.7749, "longitude": 122.4194} context_args["permissions"] = ["geolocation"] @@ -42,29 +40,28 @@ def test_task_preview(): context_args["user_agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36" context_args["ignore_https_errors"] = True + # Create first context and start tracing context = browser.new_context(**context_args) - try: - - context.tracing.start(screenshots=True, snapshots=True) + context.tracing.start(screenshots=True, snapshots=True, sources=True) + # Initialize context1 outside the try block + context1 = None + try: page = context.new_page() page.wait_for_timeout(5000) - page.goto("https://127.0.0.1:8082/") - email1 = create_user(page, "Test", "User1", "t1533148+1734358702@gmail.com") - - + + # Create a second context and start tracing for it context1 = browser.new_context(**context_args) - - context1.tracing.start(screenshots=True, snapshots=True) + context1.tracing.start(screenshots=True, snapshots=True, sources=True) page1 = context1.new_page() page1.wait_for_timeout(5000) - page1.goto("https://127.0.0.1:8082/") email2 = create_user(page1, "Test", "User2", "t1533148+1734358725@gmail.com") + page.wait_for_timeout(2000) page.get_by_label("My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").click() @@ -111,27 +108,29 @@ def test_task_preview(): page.wait_for_timeout(5000) expect(page.locator("text=Approve what's not on hold, or approve the entire report.")).not_to_be_visible() - - page1.close() - page.close() - context1.close() - context.close() - - browser.close() - except Exception as e: - raise e finally: - + # Stop tracing and export trace files for each context if available + if context1 is not None: + context1.tracing.stop(path="/app/expensify/user_tool/output_browser2.zip") + trace_cleaner("/app/expensify/user_tool/output_browser2.zip") + try: + context1.close() + except Exception: + pass + context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") try: - context.tracing.stop() - context.tracing.export("/app/expensify/user_tool/output_browser1.zip") - trace_cleaner("/app/expensify/user_tool/output_browser1.zip") - except: # In case anything goes wrong stopping or exporting the trace + # Attempt to close pages if they exist + page1.close() + except Exception: pass - try: - context1.tracing.stop() - context1.tracing.export("/app/expensify/user_tool/output_browser2.zip") - trace_cleaner("/app/expensify/user_tool/output_browser2.zip") - except: + page.close() + except Exception: pass + context.close() + browser.close() + + +if __name__ == "__main__": + test_task_preview() diff --git a/issues/41771_458/user_tool.py b/issues/41771_458/user_tool.py index a53edd5..34ad238 100644 --- a/issues/41771_458/user_tool.py +++ b/issues/41771_458/user_tool.py @@ -7,8 +7,8 @@ from playwright._impl._page import Page from utils.trace_cleaner import trace_cleaner -def launch_browser(playwright, platform="desktop"): +def launch_browser(playwright, platform="desktop"): mobile_context = {} if platform.lower() == "mobile": @@ -18,14 +18,15 @@ def launch_browser(playwright, platform="desktop"): } browser = playwright.chromium.launch(headless=True, args=[ - "--ignore-certificate-errors", - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ]) + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process" + ]) context = browser.new_context(**mobile_context) + context.tracing.start(screenshots=True, snapshots=True, sources=True) page = context.new_page() - return page + return browser, context, page def login_user_if_not_logged_in(page: Page, platform="desktop"): @@ -42,9 +43,7 @@ def login_user_if_not_logged_in(page: Page, platform="desktop"): def verify_receipt_violation_error_message(page: Page, member_name: str, approval_required_amount: int): - page.get_by_label("Navigates to a chat").get_by_text( - member_name, exact=True - ).click() + page.get_by_label("Navigates to a chat").get_by_text(member_name, exact=True).click() page_id = page.url.rstrip("/").split("/")[-1] page.get_by_test_id(f"report-screen-{page_id}").get_by_role( "button", name="View details" @@ -57,39 +56,25 @@ def verify_receipt_violation_error_message(page: Page, member_name: str, approva "The approval limit should not be undefined", ).to_have_count(0) - approval_error_message = page.get_by_text( - f"Expense exceeds auto approval limit of ${approval_required_amount}" - ) if page.get_by_text( - f"Expense exceeds auto approval limit of ${approval_required_amount}" - ).is_visible() else page.get_by_text( - f"Expense exceeds auto approval limit of ${approval_required_amount}" - ).nth(1) + approval_error_message = (page.get_by_text(f"Expense exceeds auto approval limit of ${approval_required_amount}") + if page.get_by_text(f"Expense exceeds auto approval limit of ${approval_required_amount}").is_visible() + else page.get_by_text(f"Expense exceeds auto approval limit of ${approval_required_amount}").nth(1)) - expect(approval_error_message,"The approval limit should be correct",).to_have_count(1) + expect(approval_error_message, "The approval limit should be correct").to_have_count(1) def test_receipt_violation_should_show_correct_error_message(): playwright = sync_playwright().start() - page = None - context = None - + browser, context, page = launch_browser(playwright) try: - page = launch_browser(playwright) - context = page.context - context.tracing.start(screenshots=True, snapshots=True, sources=True) - login_user_if_not_logged_in(page) page.wait_for_timeout(2000) verify_receipt_violation_error_message(page, "Asaad J", 100) - - except Exception as e: - raise e - finally: - if context: - context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) - trace_cleaner(trace_path) - context.close() + context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + browser.close() playwright.stop() + + +test_receipt_violation_should_show_correct_error_message() diff --git a/issues/42556_686/user_tool.py b/issues/42556_686/user_tool.py index 2b41607..47ddea2 100644 --- a/issues/42556_686/user_tool.py +++ b/issues/42556_686/user_tool.py @@ -3,6 +3,8 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import time import imaplib import email @@ -16,9 +18,6 @@ from enum import Enum from datetime import datetime, timezone -from utils.trace_cleaner import trace_cleaner # Added import for trace_cleaner - - TEST_URL = "https://dev.new.expensify.com:8082/" # TODO Use the URL that works in your local development. For example, it can be 'https://dev.new.expensify.com:8082/' @@ -27,18 +26,10 @@ - - - - - - - USER_NAME = "expensify31" # TODO Change this to your Gmail username USER_PASSWORD = "Pass@123456" # TODO Change this to your password - class TodayOptions(Enum): TRACK_AND_BUDGET_EXPENSES = 1 SOMETHING_ELSE = 4 @@ -64,8 +55,6 @@ def wait(page, for_seconds=1): - - def get_magic_code(user_email, password, retries=5, delay=10, since=None): if since is None: since = datetime.now(timezone.utc).replace(second=0, microsecond=0) @@ -159,7 +148,6 @@ def choose_link_if_any(page, link_text, retries = 5): expect(link).to_be_visible() link.click() - def login(p: PlaywrightContextManager, user_info, if_phone=False) -> tuple[Browser, Page, str]: login_time = datetime.now(timezone.utc).replace(second=0, microsecond=0) @@ -178,7 +166,7 @@ def login(p: PlaywrightContextManager, user_info, if_phone=False) -> tuple[Brows page = context.new_page() - page.goto(TEST_URL, timeout=120000) # Timeout is set to 120 seconds + page.goto(TEST_URL, timeout=120000) # Timeout is set to 120 seconds to avoid a Timeout error in my environment - native MacOS M1 Max. Perhaps you can set it to a lower value if you have a faster environment. phone_or_email_input = page.locator('input[type="email"]') expect(phone_or_email_input).to_be_visible() @@ -206,26 +194,22 @@ def login(p: PlaywrightContextManager, user_info, if_phone=False) -> tuple[Brows expect(validate_code_input).to_be_visible() validate_code_input.fill(magic_code) - return browser, page + return browser, page - def test(): with sync_playwright() as p: - - user_info = get_test_user_info(seed=None) - + user_info = get_test_user_info(seed=None) # TODO Use any seed that you like browser, page = login(p, user_info, True) - context = page.context - + # Start tracing + context = page.context() context.tracing.start(screenshots=True, snapshots=True, sources=True) - + trace_path = "/app/expensify/user_tool/output_browser1.zip" try: - choose_what_to_do_today_if_any(page, TodayOptions.SOMETHING_ELSE, **user_info) - choose_link_if_any(page, "Get started") + choose_link_if_any(page, "Get started") # A new user will see this Get Started link on the first logon. page.wait_for_timeout(2000) page.get_by_label("My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").get_by_text("Workspaces").click() @@ -251,16 +235,15 @@ def test(): page.wait_for_timeout(2000) page.get_by_role("button", name="Save").click() page.wait_for_timeout(2000) + error_selector = "text=An error occurred while" + expect(page.locator(error_selector)).not_to_be_visible(timeout=100) - except Exception as e: - print("Test failed:" , e) - raise e finally: - - context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - browser.close() + + + +test() \ No newline at end of file diff --git a/issues/42934_932/user_tool.py b/issues/42934_932/user_tool.py index 742dc1e..0be136e 100644 --- a/issues/42934_932/user_tool.py +++ b/issues/42934_932/user_tool.py @@ -1,5 +1,7 @@ import sys, os +from utils.trace_cleaner import trace_cleaner + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) @@ -11,55 +13,54 @@ import unittest import time -from utils.trace_cleaner import trace_cleaner class TestSuite(unittest.TestCase): @classmethod def setUp(cls): - cls.username = 'testemail.ameen' # Email username + cls.username = 'testemail.ameen' # Email username email = cls.generate_random_email() - cls.password = "yjsp jfez uvmy gkgs" # App password + cls.password = "yjsp jfez uvmy gkgs" # App password cls.url = 'https://dev.new.expensify.com:8082/' cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch(headless=True,args=[ - '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process', - ], - ) + cls.browser = cls.playwright.chromium.launch(headless=True, args=[ + '--disable-web-security', + '--disable-features=IsolateOrigins,site-per-process', + ]) cls.context = cls.browser.new_context() + cls.context.tracing.start(screenshots=True, snapshots=True, sources=True) cls.page = cls.context.new_page() - cls.page.goto( - cls.url, - timeout=60000) + cls.page.goto(cls.url, timeout=60000) cls.login_account(email) @classmethod def tearDown(cls): - - cls.page.close() - cls.context.close() - cls.browser.close() - cls.playwright.stop() + try: + cls.context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + except Exception as e: + print(f"Error during tracing export: {e}") + finally: + cls.page.close() + cls.context.close() + cls.browser.close() + cls.playwright.stop() @classmethod def login_account(cls, email, first_name='Test', last_name='User', context_page=None): - page = page = context_page if context_page else cls.page - - + page = context_page if context_page else cls.page page.locator('input[type="email"]').fill(email) page.locator('button[tabindex="0"]').click(timeout=10000) - - try: + try: page.wait_for_selector("text=Please enter the magic code sent to", timeout=5000) - cls.existing_login=True - expensify_opt = cls.get_magic_code(email, cls.password) # Get OTP + cls.existing_login = True + expensify_opt = cls.get_magic_code(email, cls.password) # Get OTP page.locator('input[inputmode="numeric"]').fill(expensify_opt) page.wait_for_selector('button[tabindex="0"]:has-text("Sign in")', timeout=15000) except: - page.wait_for_selector('button[tabindex="0"]:has-text("Join")', timeout=15000) + page.wait_for_selector('button[tabindex="0"]:has-text("Join")', timeout=15000) page.locator('button[tabindex="0"]:has-text("Join")').click() try: page.locator("text='Track and budget expenses'").click() @@ -119,7 +120,6 @@ def click_element(cls, selectors_or_locator, context_page=None): page = context_page if context_page else cls.page - if isinstance(selectors_or_locator, list): element = page.locator(selectors_or_locator[0]) for selector in selectors_or_locator[1:]: @@ -152,7 +152,7 @@ def start_group_chat(cls): cls.click_element('button[aria-label="Start chat (Floating action)"]') cls.click_element('div[aria-label="Start chat"][role="menuitem"]') page.locator('input[aria-label="Name, email, or phone number"]').fill('testuser@gmail.com') - cls.navigate_with_tabs(2) # Select User + cls.navigate_with_tabs(2) # Select User cls.click_element('button:has-text("Next")') cls.click_element('button:has-text("Start group")') @@ -160,35 +160,20 @@ def start_group_chat(cls): def create_task(cls): page = cls.page cls.click_element( - page.locator('button[aria-label="Create"]').nth(1) - ) + page.locator('button[aria-label="Create"]').nth(1) + ) cls.click_element(page.get_by_text("Assign task")) page.locator('input[aria-label="Title"]').fill("Test Task") cls.click_element( - page.locator("button", has_text="Next") - ) + page.locator("button", has_text="Next") + ) cls.click_element(page.get_by_role("button", name="Confirm task")) - def test_task_checkbox_center_vertically_with_title(self): - trace_path = "/app/expensify/user_tool/output_browser1.zip" - self.context.tracing.start(screenshots=True, snapshots=True, sources=True) try: page = self.page - - - - self.start_group_chat() - - - - self.create_task() - - - - page.wait_for_timeout(2000) task_checkbox = page.get_by_role("checkbox", name="Task").bounding_box() task_title = page.get_by_role("button", name="Task").bounding_box() @@ -196,19 +181,15 @@ def test_task_checkbox_center_vertically_with_title(self): checkbox_height = task_checkbox['height'] title_height = task_title['height'] - if task_title['height'] == 24: task_title = page.get_by_role("button", name="Task").locator('div').nth(1).bounding_box() title_height = task_title['height'] - assert task_checkbox['height'] == task_title['height'] , f"Task title and checkbox heights are not the same: {checkbox_height} != {title_height}" + assert task_checkbox['height'] == task_title['height'], f"Task title and checkbox heights are not the same: {checkbox_height} != {title_height}" except Exception as e: self.fail(f"Test failed due to error: {str(e)}") - finally: - self.context.tracing.stop() - self.context.tracing.export(path=trace_path) - trace_cleaner(trace_path) + if __name__ == "__main__": - unittest.main(verbosity=2) + unittest.main(verbosity=2) \ No newline at end of file diff --git a/issues/44040_470/user_tool.py b/issues/44040_470/user_tool.py index 0534f91..3d8b99d 100644 --- a/issues/44040_470/user_tool.py +++ b/issues/44040_470/user_tool.py @@ -14,7 +14,7 @@ EMAIL_PASSWORD = "ghka tmuf vpio patv" EXPENSIFY_URL = "https://dev.new.expensify.com:8082/" - + def login_user(email_username, page): """ Log into the Expensify app. @@ -23,7 +23,7 @@ def login_user(email_username, page): page.get_by_test_id("username").fill(email_username) page.get_by_role("button", name="Continue").click() - otp_code='101010' + otp_code = '101010' page.get_by_test_id("SignInPage").get_by_test_id("validateCode").fill(otp_code) sign_in_button = page.get_by_test_id("SignInPage").get_by_role("button", name="Sign in") page.wait_for_timeout(1000) @@ -31,6 +31,7 @@ def login_user(email_username, page): sign_in_button.click() page.wait_for_timeout(2000) + def submit_expense(page, recipient_email): @@ -46,6 +47,7 @@ def submit_expense(page, recipient_email): page.wait_for_timeout(1000) + manual_button = page.locator('button[aria-label="Manual"]').wait_for() manual_button = page.locator('button[aria-label="Manual"]') manual_button.wait_for() manual_button.click() @@ -74,30 +76,23 @@ def submit_expense(page, recipient_email): def test_expenses_members_list(): with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True, args=["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], slow_mo=500) - - context1 = browser.new_context() - context1.tracing.start(screenshots=True, snapshots=True, sources=True) + context1 = None + context2 = None try: + # Create first context and start tracing + context1 = browser.new_context() + context1.tracing.start(screenshots=True, snapshots=True, sources=True) page1 = context1.new_page() + login_user(email_username=EMAIL_USERNAME1, page=page1) submit_expense(page1, EMAIL_USERNAME2) - page1.close() - except Exception as e: - print(f"Error in context1: {e}") - finally: - context1.tracing.stop() - trace_path1 = "/app/expensify/user_tool/output_browser1.zip" - context1.tracing.export(path=trace_path1) - trace_cleaner(trace_path1) - context1.close() - - context2 = browser.new_context() - context2.tracing.start(screenshots=True, snapshots=True, sources=True) - try: + + # Continue with second context + context2 = browser.new_context() + context2.tracing.start(screenshots=True, snapshots=True, sources=True) page2 = context2.new_page() login_user(email_username=EMAIL_USERNAME2, page=page2) - page2.get_by_label("Start chat (Floating action)").click() page2.get_by_label("Start chat", exact=True).click() page2.get_by_test_id("selection-list-text-input").click() @@ -109,16 +104,14 @@ def test_expenses_members_list(): submit_expense(page2, EMAIL_USERNAME1) page2.wait_for_timeout(3000) - page2.get_by_text("Test2 owes").first.click() try: page2.get_by_label("Test2 owes").first.click() - except: + except Exception: page2.get_by_text("Test2 owes").nth(1).click() page2.get_by_label("Members").click() page2.wait_for_timeout(2000) - first_user_div = page2.locator(f'button[aria-label*="Test"]:has(div:has-text("{EMAIL_USERNAME1}"))') second_user_div = page2.locator(f'button[aria-label*="Test2"]:has(div:has-text("{EMAIL_USERNAME2}"))') first_user_div_count = first_user_div.count() @@ -126,14 +119,44 @@ def test_expenses_members_list(): assert first_user_div_count == 1, f"First user div count is {first_user_div_count}" assert second_user_div_count == 1, f"Second user div count is {second_user_div_count}" - except Exception as e: - print(f"Error in context2: {e}") + finally: - context2.tracing.stop() - trace_path2 = "/app/expensify/user_tool/output_browser2.zip" - context2.tracing.export(path=trace_path2) - trace_cleaner(trace_path2) - page2.close() - context2.close() - - browser.close() + # Export tracing and clean up for context1 + if context1 is not None: + trace_path1 = "/app/expensify/user_tool/output_browser1.zip" + try: + context1.tracing.stop(path=trace_path1) + except Exception as e: + print(f"Error stopping tracing for context1: {e}") + try: + trace_cleaner(trace_path1) + except Exception as e: + print(f"Error cleaning trace for context1: {e}") + try: + context1.close() + except Exception as e: + print(f"Error closing context1: {e}") + + # Export tracing and clean up for context2 + if context2 is not None: + trace_path2 = "/app/expensify/user_tool/output_browser2.zip" + try: + context2.tracing.stop(path=trace_path2) + except Exception as e: + print(f"Error stopping tracing for context2: {e}") + try: + trace_cleaner(trace_path2) + except Exception as e: + print(f"Error cleaning trace for context2: {e}") + try: + context2.close() + except Exception as e: + print(f"Error closing context2: {e}") + try: + browser.close() + except Exception as e: + print(f"Error closing browser: {e}") + + +if __name__ == "__main__": + test_expenses_members_list() diff --git a/issues/44509_397/user_tool.py b/issues/44509_397/user_tool.py index 53be04b..a16d8c3 100644 --- a/issues/44509_397/user_tool.py +++ b/issues/44509_397/user_tool.py @@ -5,26 +5,22 @@ import pytest from playwright.sync_api import sync_playwright, Page - - from utils.trace_cleaner import trace_cleaner static_email = "testuserexpensify1+82@gmail.com" + @pytest.fixture def setup_playwright(): - playwright = sync_playwright().start() browser = playwright.chromium.launch( headless=True, slow_mo=1000, args=["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"] ) page = browser.new_page() - yield page - - browser.close() - playwright.stop() - + # Yield page, browser, and playwright so that the test can manage tracing and cleanup + yield page, browser, playwright + # No cleanup here: test function is responsible for stopping tracing, exporting, cleaning trace, and closing browser/playwright def login_user(page: Page, email: str, first_name: str, last_name: str): @@ -32,7 +28,6 @@ def login_user(page: Page, email: str, first_name: str, last_name: str): page.locator('input[type="email"]').fill(email) page.locator('button[tabindex="0"]').click() - join_button = page.get_by_role("button", name="Join") join_button.click() page.locator("text='Track and budget expenses'").click() @@ -58,7 +53,6 @@ def create_workspace_and_add_member(page: Page, member_email: str): page.locator('button[aria-label="Back"]').click() - def browse_to_workspace(page: Page, workspace_name: str): page.locator('button[aria-label="Workspaces"]').click() try: @@ -70,7 +64,6 @@ def browse_to_workspace(page: Page, workspace_name: str): ).nth(0).click() - def submit_expense(page: Page, amount: int, merchant: str): page.locator('button[aria-label="Create"]').nth(2).click() page.locator('div[aria-label="Submit expense"]').click() @@ -83,7 +76,6 @@ def submit_expense(page: Page, amount: int, merchant: str): page.locator('button[data-listener="Enter"]', has_text="Submit").click() - def wait_for_text_with_assertion( page: Page, locator, @@ -105,12 +97,9 @@ def wait_for_text_with_assertion( @pytest.mark.usefixtures("setup_playwright") def test_duplicate_and_hold_expense(setup_playwright): - page = setup_playwright - trace_path = "/app/expensify/user_tool/output_browser1.zip" - - + page, browser, playwright = setup_playwright + # Start tracing with desired options page.context.tracing.start(screenshots=True, snapshots=True, sources=True) - try: user1_email = static_email user2_email = "testuser@gmail.com" @@ -154,14 +143,9 @@ def test_duplicate_and_hold_expense(setup_playwright): interval=1, assertion_message="Expense's text not updated to 'Hold' from 'Duplicate'.", ) - - except Exception as e: - - raise e - finally: - - page.context.tracing.stop() - page.context.tracing.export(path=trace_path) - - trace_cleaner(trace_path) + trace_file = "/app/expensify/user_tool/output_browser1.zip" + page.context.tracing.stop(path=trace_file) + trace_cleaner(trace_file) + browser.close() + playwright.stop() \ No newline at end of file diff --git a/issues/44728_421/user_tool.py b/issues/44728_421/user_tool.py index a364796..e32b609 100644 --- a/issues/44728_421/user_tool.py +++ b/issues/44728_421/user_tool.py @@ -1,4 +1,5 @@ import sys, os +from utils.trace_cleaner import trace_cleaner base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) @@ -13,29 +14,24 @@ def login_user(p: PlaywrightContextManager, first_name="John", last_name="Doe") -> tuple[Browser, Page, str]: browser = p.chromium.launch(headless=True, slow_mo=1000, args=["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"]) - + page = browser.new_page() user_email = static_email - - page.goto('https://dev.new.expensify.com:8082/') - + page.goto('https://dev.new.expensify.com:8082/') page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() - try: page.locator('button[tabindex="0"]').click() except Exception: pass - page.locator("text='Something else'").click() page.get_by_role("button", name="Continue").click() - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("button", name="Continue").last.click() @@ -45,22 +41,23 @@ def login_user(p: PlaywrightContextManager, first_name="John", last_name="Doe") def test_44728(): with sync_playwright() as p: browser, page, user_email = login_user(p) - page.context.tracing.start(title="test_44728", screenshots=True, snapshots=True) + context = page.context + context.tracing.start(screenshots=True, snapshots=True, sources=True) try: personal_chat = page.get_by_text(f"(you)") personal_chat.click() - concierge_text = page.locator('text=Concierge').nth(1) - font_weight = concierge_text.evaluate("el => window.getComputedStyle(el).fontWeight") assert int(font_weight) >= 700, "The Concierge chat is not marked as unread." finally: - page.context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - page.context.tracing.export(path=trace_path) - from utils.trace_cleaner import trace_cleaner + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) browser.close() + + +if __name__ == '__main__': + test_44728() \ No newline at end of file diff --git a/issues/44804_694/user_tool.py b/issues/44804_694/user_tool.py index bcd6243..92c6e45 100644 --- a/issues/44804_694/user_tool.py +++ b/issues/44804_694/user_tool.py @@ -15,6 +15,7 @@ from utils.trace_cleaner import trace_cleaner + class EmailHandler: def __init__(self, email_address, password): @@ -74,6 +75,7 @@ def check_for_otp(self): otp_code = self._extract_otp_from_email(msg) if otp_code: + mail.close() mail.logout() return otp_code @@ -111,14 +113,14 @@ def _extract_otp(self, text): match = re.search(r"\b\d{6}\b", text) return match.group(0) if match else None + TIMEOUT = 20000 NEWDOT_URL = "https://dev.new.expensify.com:8082/" TEST_EMAIL = "turingtemp+177125@gmail.com" APP_PASSWORD = "rxyc esdd vdad pamq" -def login_user( - p: PlaywrightContextManager, email: str -) -> tuple[Browser, Page]: + +def login_user(p: PlaywrightContextManager, email: str) -> tuple[Browser, Page]: browser = p.chromium.launch(headless=True, args=[ "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" @@ -129,8 +131,10 @@ def login_user( context.set_default_timeout(TIMEOUT) context.set_default_navigation_timeout(TIMEOUT) - page = context.new_page() + # Start tracing with screenshots, snapshots, and sources + context.tracing.start(screenshots=True, snapshots=True, sources=True) + page = context.new_page() page.goto(NEWDOT_URL) @@ -172,16 +176,19 @@ def logout_user(page: Page) -> str: def test_do_not_show_account_green_indicator_for_existing_user(): with sync_playwright() as p: browser, page = login_user(p, TEST_EMAIL) - context = page.context - context.tracing.start(screenshots=True, snapshots=True, sources=True) - trace_path = "/app/expensify/user_tool/output_browser1.zip" try: account_settings_button = page.locator('button[aria-label="My settings"]') green_indicator = account_settings_button.locator('div[class="css-view-175oi2r"][style*="background-color: rgb(3, 212, 124);"][style*="border-color: rgb(252, 251, 249);"]') page.wait_for_timeout(4000) expect(green_indicator).not_to_be_visible() finally: - context.tracing.stop() - context.tracing.export(path=trace_path) + # Stop tracing and export the trace file + context = page.context() + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) browser.close() + + +if __name__ == "__main__": + test_do_not_show_account_green_indicator_for_existing_user() \ No newline at end of file diff --git a/issues/45190_520/user_tool.py b/issues/45190_520/user_tool.py index 1e34a5f..9e83d2a 100644 --- a/issues/45190_520/user_tool.py +++ b/issues/45190_520/user_tool.py @@ -13,6 +13,7 @@ import numpy as np from PIL import Image import io + from utils.trace_cleaner import trace_cleaner GMAIL_NAME = "testerbro7077" @@ -25,8 +26,8 @@ def get_test_user_info(seed=None, first_name=None, last_name=None): if last_name is None: last_name = "Sai" - email = f"{GMAIL_NAME}+{seed}@gmail.com" if seed is not None else f"{GMAIL_NAME}@gmail.com" - return {"email": email, "password": GMAIL_APP_PASSWORD, "first_name": first_name, "last_name": last_name} + email_addr = f"{GMAIL_NAME}+{seed}@gmail.com" if seed is not None else f"{GMAIL_NAME}@gmail.com" + return {"email": email_addr, "password": GMAIL_APP_PASSWORD, "first_name": first_name, "last_name": last_name} def get_magic_code(user_email, password, retries=5, delay=5): imap = imaplib.IMAP4_SSL("imap.gmail.com") @@ -94,7 +95,7 @@ def login_user(page, user_info, activity_text="Track and budget expenses"): if join_button.is_visible(): join_button.click(timeout=3000) else: - magic_code = "123456" #get_magic_code(user_info["email"], user_info["password"], retries=6, delay=5) + magic_code = "123456" # get_magic_code(user_info["email"], user_info["password"], retries=6, delay=5) print(f"Magic code: {magic_code}") validate_code_input.fill(magic_code) @@ -133,7 +134,7 @@ def chech_bg_color(page): page.wait_for_timeout(1000) view_button = page.get_by_role("button", name="View", exact=True).nth(2) - container = view_button.locator("..").locator("..").locator("..").locator("..") + container = view_button.locator("..").locator("..").locator("..").locator("..").locator() background_color = container.evaluate("element => getComputedStyle(element).backgroundColor") expected_background_color = "rgb(26, 61, 50)" @@ -148,19 +149,21 @@ def test_login(): ]) userA_info = get_test_user_info(seed=1, first_name="Yagna") contextA = browser.new_context() + # Start tracing after creating the context contextA.tracing.start(screenshots=True, snapshots=True, sources=True) pageA = contextA.new_page() + try: login_user(pageA, userA_info) chech_bg_color(pageA) - except Exception as e: - print("Test failed due to exception:", e) - raise finally: - contextA.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - contextA.tracing.export(path=trace_path) + contextA.tracing.stop(path=trace_path) trace_cleaner(trace_path) pageA.close() contextA.close() browser.close() + + +if __name__ == "__main__": + test_login() \ No newline at end of file diff --git a/issues/45523_892/user_tool.py b/issues/45523_892/user_tool.py index cb7ca50..d69814e 100644 --- a/issues/45523_892/user_tool.py +++ b/issues/45523_892/user_tool.py @@ -1,4 +1,5 @@ import sys, os +from utils.trace_cleaner import trace_cleaner base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) @@ -8,7 +9,6 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page -from utils.trace_cleaner import trace_cleaner def generate_random_email(): @@ -16,7 +16,7 @@ def generate_random_email(): return f"turingtemp+994508@gmail.com" -def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, str]: +def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, object, str]: browser = p.chromium.launch(headless=True, args=[ "--disable-web-security", @@ -26,16 +26,13 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - page = context.new_page() user_email = generate_random_email() - - page.goto('https://dev.new.expensify.com:8082/') - + page.goto('https://dev.new.expensify.com:8082/') page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() page.wait_for_timeout(2000) - - try: + try: page.locator('button[tabindex="0"]').click() page.wait_for_timeout(4000) except Exception: @@ -44,7 +41,6 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - try: expect(page.locator("text=What do you want to do today?")).to_be_visible() - page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() @@ -62,10 +58,8 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - def test_emojiart(): with sync_playwright() as p: browser, page, context, user_email = login_user(p) - context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - page.get_by_label("Start chat (Floating action)").click() page.get_by_text("Start chat").click() page.wait_for_timeout(3000) @@ -73,18 +67,13 @@ def test_emojiart(): page.get_by_role("textbox").fill("💙💗💗💗💗💗💗\n💙❤❤❤❤❤ 💛💗\n💙❤HELLO ♪❤💛💗\n💙❤👍☀👎❤ 💛💗\n💙❤☮ O˥˥ƎH❤💛💗\n💙❤❤❤❤❤ 💛💗\n💚💚💚💚💚💚💛\n") page.keyboard.press("Enter") - page.get_by_label("Emoji").nth(1).click() page.wait_for_timeout(2000) crash_message = "Uh-oh, something went wrong!" assert crash_message not in page.content(), f"Crash message '{crash_message}' was found on the page" - except Exception as e: - raise e finally: - - context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) - + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) + browser.close() \ No newline at end of file diff --git a/issues/45543_1016/user_tool.py b/issues/45543_1016/user_tool.py index 136a704..b6281d2 100644 --- a/issues/45543_1016/user_tool.py +++ b/issues/45543_1016/user_tool.py @@ -3,11 +3,13 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import pytest from playwright.async_api import expect, async_playwright, Browser, Page from utils.login import sign_in, NEW_DOT_URL, check_if_logged_in from utils.browser import close_browser -from utils.trace_cleaner import trace_cleaner + USER_EMAILA = "doorsqueaky+3182311@gmail.com" USERNAME_A = 'John Doe' @@ -31,6 +33,8 @@ async def start_browser_with_proxy(): ] ) context = await browser.new_context(ignore_https_errors=True) + # Start tracing for this context + await context.tracing.start(screenshots=True, snapshots=True, sources=True) page = await context.new_page() return context, page, playwright @@ -65,6 +69,8 @@ async def create_draft_reply_in_thread(page: Page, username: str, sender_email: await page.wait_for_timeout(1000) + message_editor = page.locator('div[aria-label="Chat message"]', has_text=message) + # Assuming the draft editor is the same as the message editor for reply message_editor = page.locator('div[contenteditable="true"][placeholder="Write something..."]').last await message_editor.fill(reply) @@ -91,56 +97,40 @@ async def enter_dummy_otp_if_not_logged_in(page: Page, email: str): await page.get_by_test_id("validateCode").fill("123456") - @pytest.mark.asyncio async def test_workspace_switch_on_reload(): - contextA, pageA, playwrightA = None, None, None - contextB, pageB, playwrightB = None, None, None + # Initialize browser contexts to None for cleanup + contextA = contextB = None + pageA = pageB = None + playwrightA = playwrightB = None try: - contextA, pageA, playwrightA = await start_browser_with_proxy() - await contextA.tracing.start(title="Browser Context A", screenshots=True, snapshots=True, sources=True) - await enter_dummy_otp_if_not_logged_in(pageA, USER_EMAILA) + await expect(pageA.get_by_label("Inbox")).to_be_visible() message = "Hello How are you" await send_message(pageA, USER_EMAILB, message) - contextB, pageB, playwrightB = await start_browser_with_proxy() - await contextB.tracing.start(title="Browser Context B", screenshots=True, snapshots=True, sources=True) - await enter_dummy_otp_if_not_logged_in(pageB, USER_EMAILB) + await expect(pageA.get_by_label("Inbox")).to_be_visible() reply = "Hey I am good. What about you?" await create_draft_reply_in_thread(pageB, USERNAME_A, USER_EMAILA, message, reply) await navigate_away_check_draft(pageB, USERNAME_A, USER_EMAILA) - except Exception as e: - - raise e finally: - - if contextA: - await contextA.tracing.stop() - trace_path_a = "/app/expensify/user_tool/output_browser1.zip" - await contextA.tracing.export(trace_path_a) - trace_cleaner(trace_path_a) - - - if contextB: - await contextB.tracing.stop() - trace_path_b = "/app/expensify/user_tool/output_browser2.zip" - await contextB.tracing.export(trace_path_b) - trace_cleaner(trace_path_b) - - - if contextA and pageA and playwrightA: + # Stop tracing and export the trace before closing each browser + if contextA is not None: + await contextA.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") await close_browser(contextA, pageA, playwrightA) - if contextB and pageB and playwrightB: - await close_browser(contextB, pageB, playwrightB) + if contextB is not None: + await contextB.tracing.stop(path="/app/expensify/user_tool/output_browser2.zip") + trace_cleaner("/app/expensify/user_tool/output_browser2.zip") + await close_browser(contextB, pageB, playwrightB) \ No newline at end of file diff --git a/issues/45882_428/user_tool.py b/issues/45882_428/user_tool.py index d58c237..01ca00c 100644 --- a/issues/45882_428/user_tool.py +++ b/issues/45882_428/user_tool.py @@ -3,14 +3,16 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import time + from playwright.sync_api import TimeoutError, expect, sync_playwright -from utils.trace_cleaner import trace_cleaner -EXPENSIFY_URL = "https://dev.new.expensify.com:8082/" -EMAIL = "t1533148@gmail.com" # for example test+1234@gmail.com -PASSWORD = "logb hzkg pkfk kskg" # it will be something like "sdib pxop ovyl uawy" +EXPENSIFY_URL = "https://dev.new.expensify.com:8082/" +EMAIL = "t1533148@gmail.com" # for example test+1234@gmail.com +PASSWORD = "logb hzkg pkfk kskg" # it will be something like "sdib pxop ovyl uawy" def generate_random_email(): @@ -31,10 +33,8 @@ def login_user(page, email, first_name="John", last_name="Doe"): try: expect(page.get_by_test_id("SignInPage").get_by_test_id("validateCode")).to_be_visible(timeout=5000) except AssertionError: - page.get_by_test_id("SignInPage").get_by_role("button", name="Join").click() else: - otp_code = '123456' page.get_by_test_id("SignInPage").get_by_test_id("validateCode").fill(otp_code) page.get_by_test_id("SignInPage").get_by_role("button", name="Sign in").click() @@ -44,7 +44,6 @@ def login_user(page, email, first_name="John", last_name="Doe"): except AssertionError: pass else: - page.get_by_label("Track and budget expenses").click() page.get_by_role("textbox", name="First name").fill(first_name) page.get_by_role("textbox", name="Last name").fill(last_name) @@ -68,47 +67,43 @@ def test_user_current_location_is_not_shown_under_recent_destinations_for_distan Verify that user's current location is not shown under recent destinations while creating a distance expense request. """ with sync_playwright() as p: + browser = p.chromium.launch(headless=True, slow_mo=500, proxy={"server": "http://127.0.0.1:8080/"}, args=[ "--ignore-certificate-errors", - "--disable-web-security", + "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process" - ] - ) + ]) geolocation = {"longitude": 41.890221, "latitude": 12.492348} - context_args = { - "geolocation": geolocation, - "permissions": ["geolocation"], - "ignore_https_errors": True - } + context_args = {} + context_args["geolocation"] = geolocation + context_args["permissions"] = ["geolocation"] + context_args["ignore_https_errors"] = True context = browser.new_context(**context_args) - page = context.new_page() - page.wait_for_timeout(1000) - email = generate_random_email() - first_name = "Fname" - last_name = "Lname" + # Start tracing immediately after context creation + context.tracing.start(screenshots=True, snapshots=True, sources=True) - context.tracing.start(title="test_trace", screenshots=True, snapshots=True) try: + page = context.new_page() + page.wait_for_timeout(1000) + email = generate_random_email() + first_name = "Fname" + last_name = "Lname" login_user(page, email, first_name=first_name, last_name=last_name) - page.get_by_role("button", name="My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_role("menuitem", name="Workspaces", exact=True).click() page.get_by_test_id("WorkspacesListPage").get_by_role("button", name="New workspace").first.click() - texts = page.get_by_test_id("WorkspacePageWithSections").get_by_role("menuitem").all_inner_texts() workspace_name = texts[0].split("\n")[-1] - page.get_by_test_id("WorkspaceInitialPage").get_by_role("button", name="Back").click() page.get_by_role("button", name="Inbox", exact=True).click() page.get_by_test_id("BaseSidebarScreen").get_by_text(workspace_name, exact=True).click() - page.get_by_test_id("report-actions-view-wrapper").get_by_role("button", name="Create", exact=True).click() page.get_by_role("menuitem", name="Submit expense", exact=True).click() page.get_by_test_id("IOURequestStartPage").get_by_role("button", name="Distance", exact=True).click() @@ -125,27 +120,21 @@ def test_user_current_location_is_not_shown_under_recent_destinations_for_distan page.get_by_test_id("IOURequestStartPage").get_by_role("button", name="Next", exact=True).last.click() page.get_by_test_id("IOURequestStepConfirmation").get_by_role("button", name="Submit", exact=False).click() - - page.reload() page.wait_for_load_state("domcontentloaded") page.wait_for_timeout(3000) - page.get_by_test_id("report-actions-view-wrapper").get_by_role("button", name="Create", exact=True).click() page.get_by_role("menuitem", name="Submit expense", exact=True).click() page.get_by_test_id("IOURequestStartPage").get_by_role("button", name="Distance", exact=True).click() page.get_by_test_id("IOURequestStartPage").get_by_role("menuitem", name="Start", exact=True).click() - expect( page.get_by_test_id("IOURequestStepWaypoint").get_by_text("Recent destinations", exact=True) ).to_be_visible(timeout=5000) - page.wait_for_timeout(3000) - current_location = f"Lat: {geolocation['latitude']} Lng: {geolocation['longitude']}" assert_message = "The user current location must not be shown under recent destinations" expect( @@ -153,11 +142,16 @@ def test_user_current_location_is_not_shown_under_recent_destinations_for_distan assert_message, ).not_to_be_visible() - finally: + page.wait_for_timeout(3000) - context.tracing.stop() + finally: + # Stop tracing and export trace file trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path) + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) context.close() browser.close() + + +if __name__ == '__main__': + test_user_current_location_is_not_shown_under_recent_destinations_for_distance_expense_request() \ No newline at end of file diff --git a/issues/46053_566/user_tool.py b/issues/46053_566/user_tool.py index 75135bc..2b43782 100644 --- a/issues/46053_566/user_tool.py +++ b/issues/46053_566/user_tool.py @@ -9,32 +9,29 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page - from utils.trace_cleaner import trace_cleaner static_email = "testuserexpensify1+44@gmail.com" + def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, str]: browser = p.chromium.launch(headless=True, slow_mo=1000, args=["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"]) page = browser.new_page() user_email = static_email - page.goto('https://dev.new.expensify.com:8082/') - page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() page.wait_for_timeout(1000) - try: page.locator('button[tabindex="0"]').click() page.wait_for_timeout(1000) except Exception: pass - + page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() @@ -55,76 +52,56 @@ def test_close_account_bottom_margin(): last_name = 'Doe' new_workspace_name = 'Testing workspace' - browser, page, user_email = login_user(p, first_name, last_name) - - context = browser.new_context() - - context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - profile = page.locator('button[aria-label="My settings"]') profile.click() page.wait_for_timeout(1000) - troubleshoot = page.locator('div[role="menuitem"]').nth(9) troubleshoot.click() page.wait_for_timeout(1000) - forced_offline = page.locator('button[aria-label="Force offline"]') forced_offline.click() page.wait_for_timeout(1000) - plus_icon = page.locator('button[aria-label="Start chat (Floating action)"]') plus_icon.click() page.wait_for_timeout(2000) - new_workspace_button = page.locator('div[aria-label="New workspace"]') new_workspace_button.click() page.wait_for_timeout(1000) - + workspace_name = f"{first_name} {last_name}'s Workspace" - profile = page.locator(f'div[aria-label="{workspace_name}"]') profile.click() page.wait_for_timeout(1000) - page.locator('input[aria-label="Name"]').fill(new_workspace_name) save_button = page.locator('button', has_text="Save") save_button.click() page.wait_for_timeout(1000) - new_profile = page.locator(f'div[aria-label="{new_workspace_name}"]') page.wait_for_timeout(1000) profile_opacity = new_profile.evaluate( 'element => window.getComputedStyle(element.parentElement.parentElement.parentElement.parentElement).opacity' ) - + if profile_opacity == '0.5': assert True, 'Profile is grayed out.' else: assert False, 'Profile is not grayed out.' - + page.wait_for_timeout(1000) finally: - - context.tracing.stop() - trace_path = '/app/expensify/user_tool/output_browser1.zip' - context.tracing.export(path=trace_path) - - - trace_cleaner(trace_path) - - - browser.close() + context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + browser.close() \ No newline at end of file diff --git a/issues/46344_753/user_tool.py b/issues/46344_753/user_tool.py index 8aaa982..41b927a 100644 --- a/issues/46344_753/user_tool.py +++ b/issues/46344_753/user_tool.py @@ -6,26 +6,26 @@ from playwright.sync_api import sync_playwright from utils.trace_cleaner import trace_cleaner + def generate_random_email1(base_email="testerbro7077@gmail.com"): timestamp = int("46344") email_user, domain = base_email.split('@') return f"{email_user}+{timestamp}@{domain}" + def login_user1(p, first_name="Yagna", slow_mo=1000, last_name="Sai") -> tuple: user_email1 = generate_random_email1() password = "xqxz mmcb tvkn lpgp" - + browser = p.chromium.launch(headless=True, args=[ - '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process', - ]) + '--disable-web-security', + '--disable-features=IsolateOrigins,site-per-process', + ]) context = browser.new_context() - context.tracing.start(screenshots=True, snapshots=True, sources=True) - page = context.new_page() - page.goto('https://dev.new.expensify.com:8082/') + page.goto('https://dev.new.expensify.com:8082/') phone_or_email_input = page.locator('input[type="email"]') phone_or_email_input.fill(user_email1) @@ -33,31 +33,30 @@ def login_user1(p, first_name="Yagna", slow_mo=1000, last_name="Sai") -> tuple: page.wait_for_timeout(1000) validate_code_input = page.locator('input[name="validateCode"]').nth(0) - validate_code_input.fill("123456") return browser, context, page, user_email1 + def generate_random_email2(base_email="testerbro7077@gmail.com"): timestamp = int("46345") email_user, domain = base_email.split('@') return f"{email_user}+{timestamp}@{domain}" + def login_user2(p, first_name="Yagna", last_name="Sai") -> tuple: user_email2 = generate_random_email2() password = "xqxz mmcb tvkn lpgp" - + browser = p.chromium.launch(headless=True, args=[ - '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process', - ]) + '--disable-web-security', + '--disable-features=IsolateOrigins,site-per-process', + ]) context = browser.new_context() - context.tracing.start(screenshots=True, snapshots=True, sources=True) - page = context.new_page() - page.goto('https://dev.new.expensify.com:8082/') + page.goto('https://dev.new.expensify.com:8082/') phone_or_email_input = page.locator('input[type="email"]') phone_or_email_input.fill(user_email2) @@ -68,6 +67,7 @@ def login_user2(p, first_name="Yagna", last_name="Sai") -> tuple: return browser, context, page, user_email2 + def add_member(page): page.get_by_label("My settings").click() page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").get_by_text("Workspaces").click() @@ -77,35 +77,26 @@ def add_member(page): page.get_by_test_id("WorkspacesListPage").locator("button").filter(has_text="Testerbro7077+46345@gmail.com").click() assert not page.get_by_label("Delete").is_visible() + def test_run(admin_first_name="Yagna", admin_last_name="Sai", member_first_name="Sai", member_last_name="Yagna"): with sync_playwright() as p: - browser_member = browser_admin = None - context_member = context_admin = None - try: - browser_member, context_member, page_member, user_email1 = login_user1(p, member_first_name, member_last_name) - browser_admin, context_admin, page_admin, admin_email = login_user2(p, admin_first_name, admin_last_name) + # Launch both browsers and start tracing + browser_member, context_member, page_member, user_email1 = login_user1(p, member_first_name, member_last_name) + browser_admin, context_admin, page_admin, admin_email = login_user2(p, admin_first_name, admin_last_name) + try: add_member(page_member) - except Exception as e: - - raise e finally: + # Stop tracing for admin browser, export and clean the trace + context_admin.tracing.stop(path="/app/expensify/user_tool/output_browser2.zip") + trace_cleaner("/app/expensify/user_tool/output_browser2.zip") + browser_admin.close() + + # Stop tracing for member browser, export and clean the trace + context_member.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + browser_member.close() - if context_member: - context_member.tracing.stop() - trace_path1 = "/app/expensify/user_tool/output_browser1.zip" - context_member.tracing.export(path=trace_path1) - trace_cleaner(trace_path1) - if context_admin: - context_admin.tracing.stop() - trace_path2 = "/app/expensify/user_tool/output_browser2.zip" - context_admin.tracing.export(path=trace_path2) - trace_cleaner(trace_path2) - - if browser_member: - browser_member.close() - if browser_admin: - browser_admin.close() if __name__ == "__main__": test_run() diff --git a/issues/46371_1127/user_tool.py b/issues/46371_1127/user_tool.py index f94b938..8fb2b34 100644 --- a/issues/46371_1127/user_tool.py +++ b/issues/46371_1127/user_tool.py @@ -1,5 +1,7 @@ import sys, os +from utils.trace_cleaner import trace_cleaner + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) @@ -9,25 +11,30 @@ import random import re -from utils.trace_cleaner import trace_cleaner @pytest.fixture(scope="function") def page(): - with sync_playwright() as p: - browser = p.chromium.launch(channel="chrome", headless=True, args=[ - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process", - ], slow_mo=500) - context_args = {"viewport": {"width": 1024, "height": 640}} - context_args["timezone_id"] = "Asia/Dhaka" - page = browser.new_page(**context_args) - yield page - browser.close() + # Start Playwright explicitly so we can manage closing later via the test + p = sync_playwright().start() + browser = p.chromium.launch(channel="chrome", headless=True, args=[ + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process", + ], slow_mo=500) + context_args = {"viewport": {"width": 1024, "height": 640}, "timezone_id": "Asia/Dhaka"} + # Create a browser context so we can use tracing + context = browser.new_context(**context_args) + page = context.new_page() + # Attach browser and context to the page so we can access them later + page.browser = browser + page._context = context + yield page + # Do not close the browser here as it will be closed in the test's finally block + p.stop() def test_check_unhold_button_exists(page): - - page.context.tracing.start(screenshots=True, snapshots=True, sources=True) + # Start tracing + page._context.tracing.start(screenshots=True, snapshots=True, sources=True) try: first_name = 'Test' last_name = 'Name' @@ -36,7 +43,7 @@ def test_check_unhold_button_exists(page): page.goto('https://dev.new.expensify.com:8082/') phone_or_email_input = page.locator('input[type="email"]') expect(phone_or_email_input).to_be_visible() - + email = "rgarciatammy4+173307430716@gmail.com" phone_or_email_input.fill(email) @@ -62,7 +69,6 @@ def test_check_unhold_button_exists(page): else: workspace_name = f"{email.capitalize()}'s Workspace" - settings = page.locator('button[aria-label="My settings"]') expect(settings).to_be_visible() settings.click() @@ -93,7 +99,7 @@ def test_check_unhold_button_exists(page): page.get_by_role("textbox", name="Merchant").fill("merchant1") page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Submit").click() - + page.get_by_role("button", name="Create").click() page.get_by_label("Submit expense").get_by_text("Submit expense").click() @@ -104,7 +110,7 @@ def test_check_unhold_button_exists(page): page.get_by_role("textbox", name="Merchant").fill("merchant2") page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Submit").click() - + page.get_by_role("button", name="Create").click() page.get_by_label("Submit expense").get_by_text("Submit expense").click() @@ -146,7 +152,7 @@ def test_check_unhold_button_exists(page): page.wait_for_timeout(1000) page.keyboard.press('Enter') - page.get_by_role("button", name="View details").get_by_text(f'{workspace_name} owes').click() + page.get_by_role("button", name=f'{workspace_name} owes').click() page.wait_for_timeout(1000) page.get_by_label("Cash").get_by_text('Hold').nth(1).click() @@ -155,15 +161,12 @@ def test_check_unhold_button_exists(page): page.mouse.click(0,0) page.get_by_role('button', name='for merchant1').nth(1).click() page.wait_for_timeout(1000) - + expect(page.get_by_role('button', name='Unhold')).to_be_visible(timeout=10) - - except Exception as e: - raise e finally: - - page.context.tracing.stop() - trace_path = '/app/expensify/user_tool/output_browser1.zip' - page.context.tracing.export(trace_path) + # Stop tracing, export trace then clean up and close the browser + trace_path = "/app/expensify/user_tool/output_browser1.zip" + page._context.tracing.stop(path=trace_path) trace_cleaner(trace_path) + page.browser.close() \ No newline at end of file diff --git a/issues/46383_999/user_tool.py b/issues/46383_999/user_tool.py index 0e7c931..46f7cc2 100644 --- a/issues/46383_999/user_tool.py +++ b/issues/46383_999/user_tool.py @@ -7,8 +7,7 @@ from playwright.sync_api._context_manager import PlaywrightContextManager from playwright._impl._browser import Browser from playwright._impl._page import Page - -from utils.trace_cleaner import trace_cleaner # <--- Added import +from utils.trace_cleaner import trace_cleaner SITE_URL = "https://dev.new.expensify.com:8082/" PRIMARY_EMAIL = "expensifyolddot+1733039252@gmail.com" @@ -16,8 +15,7 @@ LAST_NAME = "1733039252" -def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, str]: - +def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") -> tuple[Browser, Page, object, str]: browser = p.chromium.launch(headless=True, proxy={'server': 'http://127.0.0.1:8080'}, args=[ @@ -36,16 +34,13 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - page = context.new_page() user_email = PRIMARY_EMAIL - page.goto(SITE_URL) - page.locator('input[type="email"]').fill(user_email) page.locator('button[tabindex="0"]').click() page.wait_for_timeout(2000) - - try: + try: page.locator('button[tabindex="0"]').click() except Exception: pass @@ -55,7 +50,6 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("button", name="Continue").last.click() @@ -67,36 +61,23 @@ def login_user(p: PlaywrightContextManager, first_name="Milan", last_name="T") - def test_create_workspace(): with sync_playwright() as p: browser, page, context, user_email = login_user(p, FIRST_NAME, LAST_NAME) - - - context.tracing.start( - title="test_create_workspace", - snapshots=True, - screenshots=True, - sources=True - ) + # Start tracing before executing the test logic + context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - if page.get_by_label("Close").is_visible(): page.get_by_label("Close").click() page.get_by_label("Workspaces").click() page.get_by_label("New workspace").click() - page.get_by_test_id("WorkspaceInitialPage").get_by_label("Back").click() + expected_url = "https://dev.new.expensify.com:8082/workspace-switcher" page.wait_for_url(expected_url) + assert page.url == expected_url, f"Expected URL to be {expected_url}, but got {page.url}" - except Exception as e: - print(f"Test failed: {e}") - raise finally: - - context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path) - trace_cleaner(trace_path) - - - context.close() - browser.close() + # Stop tracing and export trace to the output zip file + trace_output_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_output_path) + trace_cleaner(trace_output_path) + browser.close() \ No newline at end of file diff --git a/issues/48694_681/user_tool.py b/issues/48694_681/user_tool.py index 6ecda29..65de028 100644 --- a/issues/48694_681/user_tool.py +++ b/issues/48694_681/user_tool.py @@ -1,4 +1,5 @@ import sys, os +from utils.trace_cleaner import trace_cleaner base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) @@ -8,8 +9,6 @@ import re import random -from utils.trace_cleaner import trace_cleaner - NEWDOT_URL = "https://dev.new.expensify.com:8082/" EMAIL = "dev091197@gmail.com" task_id = 48694 @@ -17,135 +16,142 @@ PASSWORD = "ptul fwbd fvze yzva" class IMAPOTPExtractor: - def __init__(self, email_address, password): - if not self._validate_email(email_address): - raise ValueError("Invalid email address format.") - self.original_email = email_address - self.password = password - random_suffix = random.randint(1000, 9999) - current_timestamp = int(time.time()) - random_suffix = f"{current_timestamp}{random_suffix}" - def _validate_email(self, email_address): - return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email_address)) - def set_new_email(self, email): - self.new_email = email - def check_for_otp(self): - default_otp_code="123456" - return default_otp_code - def _extract_otp_from_email(self, msg): - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain": - email_body = part.get_payload(decode=True).decode() - otp_code = self._extract_otp(email_body) - if otp_code: - return otp_code - else: - email_body = msg.get_payload(decode=True).decode() - otp_code = self._extract_otp(email_body) - if otp_code: - return otp_code - return None - def _extract_otp(self, text): - match = re.search(r"\b\d{6}\b", text) - return match.group(0) if match else None + def __init__(self, email_address, password): + if not self._validate_email(email_address): + raise ValueError("Invalid email address format.") + self.original_email = email_address + self.password = password + random_suffix = random.randint(1000, 9999) + current_timestamp = int(time.time()) + random_suffix = f"{current_timestamp}{random_suffix}" + + def _validate_email(self, email_address): + return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email_address)) + + def set_new_email(self, email): + self.new_email = email + + def check_for_otp(self): + default_otp_code = "123456" + return default_otp_code + + def _extract_otp_from_email(self, msg): + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == "text/plain": + email_body = part.get_payload(decode=True).decode() + otp_code = self._extract_otp(email_body) + if otp_code: + return otp_code + else: + email_body = msg.get_payload(decode=True).decode() + otp_code = self._extract_otp(email_body) + if otp_code: + return otp_code + return None + + def _extract_otp(self, text): + match = re.search(r"\b\d{6}\b", text) + return match.group(0) if match else None + gmail_account_helper = IMAPOTPExtractor(EMAIL, PASSWORD) def new_dot_login(page, email): - page.goto(NEWDOT_URL) - page.locator('input[type="email"]').fill(email) - page.wait_for_timeout(2000) - page.get_by_role("button", name="Continue").nth(0).click() - page.wait_for_timeout(10000) - gmail_account_helper.set_new_email(email) - otp = gmail_account_helper.check_for_otp() - page.locator('input[data-testid="validateCode"]').fill(otp) - try: - page.get_by_role("button", name="Sign In").click() - except: - pass + page.goto(NEWDOT_URL) + page.locator('input[type="email"]').fill(email) + page.wait_for_timeout(2000) + page.get_by_role("button", name="Continue").nth(0).click() + page.wait_for_timeout(10000) + gmail_account_helper.set_new_email(email) + otp = gmail_account_helper.check_for_otp() + page.locator('input[data-testid="validateCode"]').fill(otp) + try: + page.get_by_role("button", name="Sign In").click() + except: + pass + def task_check(page): - random_int_1 = 13 - random_int_2 = 21 - workspace_name = f"WS-{random_int_1}{random_int_2}" - room_name = f"r-{random_int_1}{random_int_2}" - email_1 = f"dev091197+{random_int_1}+{random_int_2}@gmail.com" - test_name = "test inzi" - page.get_by_label("My settings").click() - page.wait_for_timeout(2000) - page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").click() - page.get_by_role("button", name="New workspace").first.click() - page.wait_for_timeout(1000) - page.get_by_test_id("WorkspacePageWithSections").get_by_text("Name").click() - page.wait_for_timeout(1000) - page.get_by_role("textbox", name="Name").press("ControlOrMeta+a") - page.wait_for_timeout(1000) - page.get_by_role("textbox", name="Name").fill(workspace_name) - page.wait_for_timeout(1000) - page.get_by_role("button", name="Save").click() - page.wait_for_timeout(1000) - page.get_by_label("Back").click() - page.get_by_label("Inbox").click() - page.wait_for_timeout(1000) - page.get_by_label("Start chat (Floating action)").click() - page.get_by_label("Start chat", exact=True).click() - page.wait_for_timeout(1000) - page.get_by_label("Room").first.click() - page.get_by_placeholder("social").fill(room_name) - page.wait_for_timeout(1000) - page.get_by_test_id("WorkspaceNewRoomPage").get_by_label("WS-").get_by_text("Workspace").click() - page.get_by_test_id("ValueSelectorModal").get_by_label(workspace_name).click() - page.wait_for_timeout(1000) - page.get_by_role("button", name="Create room").click() - page.get_by_label(f"#{room_name}").click() - page.wait_for_timeout(1000) - page.get_by_test_id("ReportDetailsPage").get_by_label("Settings").click() - page.get_by_text("Notify me about new messages").click() - page.wait_for_timeout(1000) - page.get_by_test_id("selection-list").get_by_label("Daily").click() - page.get_by_test_id("ReportSettingsPage").get_by_label("Back").click() - page.wait_for_timeout(1000) - page.get_by_label("Leave").click() - page.wait_for_timeout(1000) - page.locator("#composer").last.click() - page.locator("#composer").last.fill(f"message-{random_int_1}{random_int_2}") - page.wait_for_timeout(1000) - page.get_by_label("Send").last.click() - page.get_by_label(f"#{room_name}").click() - page.wait_for_timeout(1000) - page.get_by_test_id("ReportDetailsPage").get_by_label("Settings").click() - page.wait_for_timeout(2000) - expect(page.get_by_text("Daily")).to_be_visible() + random_int_1 = 13 + random_int_2 = 21 + workspace_name = f"WS-{random_int_1}{random_int_2}" + room_name = f"r-{random_int_1}{random_int_2}" + email_1 = f"dev091197+{random_int_1}+{random_int_2}@gmail.com" + test_name = "test inzi" + page.get_by_label("My settings").click() + page.wait_for_timeout(2000) + page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").click() + page.get_by_role("button", name="New workspace").first.click() + page.wait_for_timeout(1000) + page.get_by_test_id("WorkspacePageWithSections").get_by_text("Name").click() + page.wait_for_timeout(1000) + page.get_by_role("textbox", name="Name").press("ControlOrMeta+a") + page.wait_for_timeout(1000) + page.get_by_role("textbox", name="Name").fill(workspace_name) + page.wait_for_timeout(1000) + page.get_by_role("button", name="Save").click() + page.wait_for_timeout(1000) + page.get_by_label("Back").click() + page.get_by_label("Inbox").click() + page.wait_for_timeout(1000) + page.get_by_label("Start chat (Floating action)").click() + page.get_by_label("Start chat", exact=True).click() + page.wait_for_timeout(1000) + page.get_by_label("Room").first.click() + page.get_by_placeholder("social").fill(room_name) + page.wait_for_timeout(1000) + page.get_by_test_id("WorkspaceNewRoomPage").get_by_label("WS-").get_by_text("Workspace").click() + page.get_by_test_id("ValueSelectorModal").get_by_label(workspace_name).click() + page.wait_for_timeout(1000) + page.get_by_role("button", name="Create room").click() + page.get_by_label(f"#{room_name}").click() + page.wait_for_timeout(1000) + page.get_by_test_id("ReportDetailsPage").get_by_label("Settings").click() + page.get_by_text("Notify me about new messages").click() + page.wait_for_timeout(1000) + page.get_by_test_id("selection-list").get_by_label("Daily").click() + page.get_by_test_id("ReportSettingsPage").get_by_label("Back").click() + page.wait_for_timeout(1000) + page.get_by_label("Leave").click() + page.wait_for_timeout(1000) + page.locator("#composer").last.click() + page.locator("#composer").last.fill(f"message-{random_int_1}{random_int_2}") + page.wait_for_timeout(1000) + page.get_by_label("Send").last.click() + page.get_by_label(f"#{room_name}").click() + page.wait_for_timeout(1000) + page.get_by_test_id("ReportDetailsPage").get_by_label("Settings").click() + page.wait_for_timeout(2000) + expect(page.get_by_text("Daily")).to_be_visible() + def test_expensify_0000(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True, args=[ + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=[ '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', ]) - - first_user_context = browser.new_context(ignore_https_errors=True) - - first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) - - page = first_user_context.new_page() - page.wait_for_timeout(2000) - - try: - new_dot_login(page, EMAIL1) - page.wait_for_timeout(2000) - task_check(page) - except Exception as e: - - raise e - finally: - - first_user_context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - first_user_context.tracing.export(trace_path) - - trace_cleaner(trace_path) - - browser.close() + + first_user_context = browser.new_context( + ignore_https_errors=True + ) + + # Start tracing with specified settings + first_user_context.tracing.start(screenshots=True, snapshots=True, sources=True) + + page = first_user_context.new_page() + page.wait_for_timeout(2000) + + try: + new_dot_login(page, EMAIL1) + page.wait_for_timeout(2000) + task_check(page) + finally: + # Stop tracing and export the trace file + first_user_context.tracing.stop(path="/app/expensify/user_tool/output_browser1.zip") + trace_cleaner("/app/expensify/user_tool/output_browser1.zip") + browser.close() + + +test_expensify_0000() \ No newline at end of file diff --git a/issues/49260_543/user_tool.py b/issues/49260_543/user_tool.py index 52d23b3..b22d576 100644 --- a/issues/49260_543/user_tool.py +++ b/issues/49260_543/user_tool.py @@ -258,6 +258,5 @@ async def test_tags_disabled_button(browser_context): print("Test failed:", e) raise e finally: - await context.tracing.stop() - await context.tracing.export(trace_path) + await context.tracing.stop(path=trace_path) trace_cleaner(trace_path) diff --git a/issues/49298_510/user_tool.py b/issues/49298_510/user_tool.py index 3e4b405..efadcfc 100644 --- a/issues/49298_510/user_tool.py +++ b/issues/49298_510/user_tool.py @@ -3,6 +3,8 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import time import imaplib import logging @@ -12,7 +14,6 @@ import string, random import pytest from playwright.sync_api import sync_playwright, expect, Page -from utils.trace_cleaner import trace_cleaner EXPENSIFY_URL = "https://dev.new.expensify.com:8082/" @@ -28,6 +29,7 @@ logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s") LOGGER = logging.getLogger(__name__) + def generate_user_email(user_id=None): """ Generate an email address for a user login. @@ -37,6 +39,7 @@ def generate_user_email(user_id=None): domain = temp[1].strip() return f"{username}+{EMAIL_USERNAME_POSTFIX}@{domain}" + def clear_inbox(username, password): """ Delete all existing messages from the Inbox. @@ -45,10 +48,11 @@ def clear_inbox(username, password): with imaplib.IMAP4_SSL(host="imap.gmail.com") as imap: imap.login(username, password) imap.select("inbox") - imap.store("1:*", "+FLAGS", "\\Deleted") + imap.store("1:*", "+FLAGS", "\Deleted") imap.expunge() imap.close() + def get_otp_from_email(username, password, retries=2, delay=2): """ Read the OTP email and return the OTP code. @@ -75,6 +79,7 @@ def get_otp_from_email(username, password, retries=2, delay=2): imap.close() return "123456" + def launch_browser(pw, headless=True, device=None, geolocation=None): """ Launch the browser. @@ -82,7 +87,7 @@ def launch_browser(pw, headless=True, device=None, geolocation=None): browser = pw.chromium.launch( headless=True, proxy={ - 'server': 'http://127.0.0.1:8080', + 'server': 'http://127.0.0.1:8080', }, args=[ '--disable-web-security', @@ -99,6 +104,7 @@ def launch_browser(pw, headless=True, device=None, geolocation=None): page = context.new_page() return browser, context, page + def login_user(page, email, first_name="John", last_name="Doe"): """ Login to the Expensify app and complete the onboarding. @@ -115,14 +121,16 @@ def login_user(page, email, first_name="John", last_name="Doe"): otp_code = get_otp_from_email(EMAIL_USERNAME, EMAIL_PASSWORD) page.get_by_test_id("validateCode").fill(otp_code) + def test(): with sync_playwright() as p: email = generate_user_email() LOGGER.info("User email %s", email) browser, context, page = launch_browser(p) + + # Start tracing the context + context.tracing.start(screenshots=True, snapshots=True, sources=True) - - context.tracing.start(screenshots=True, snapshots=True) try: login_user(page, email) page.wait_for_timeout(5000) @@ -133,7 +141,6 @@ def test(): 'button[aria-label="Start chat (Floating action)"][tabindex="0"]' ).click() - page.locator('div[aria-label="Start chat"][role="menuitem"]').click() page.locator('input[aria-label="Name, email, or phone number"]').fill( @@ -160,18 +167,16 @@ def test(): page.wait_for_selector('div[data-testid="PrivateNotesListPage"]') parent_div = page.locator('div[data-testid="PrivateNotesListPage"]') - child_with_scroll_y = parent_div.evaluate_handle( """parent => { - const children = parent.querySelectorAll("div"); - return Array.from(children).find(child => { - const style = getComputedStyle(child); - return style.overflowY === "scroll" || style.overflowY === "auto"; - }); - }""" + const children = parent.querySelectorAll("div"); + return Array.from(children).find(child => { + const style = getComputedStyle(child); + return style.overflowY === "scroll" || style.overflowY === "auto"; + }); + }""" ) - if child_with_scroll_y: text = child_with_scroll_y.evaluate("element => element.innerText") print(text) @@ -182,11 +187,13 @@ def test(): "Keep notes about this chat here. You're the only person who can add, edit, or view these notes." in text ), "The intro text on private notes list stays fixed when it should scroll with the page" - except Exception as e: - raise e finally: - - context.tracing.stop() trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path) + context.tracing.stop() + context.tracing.export_trace(trace_path) trace_cleaner(trace_path) + browser.close() + + +if __name__ == '__main__': + test() \ No newline at end of file diff --git a/issues/49492_670/user_tool.py b/issues/49492_670/user_tool.py index d19b6f0..be6bbc1 100644 --- a/issues/49492_670/user_tool.py +++ b/issues/49492_670/user_tool.py @@ -3,6 +3,8 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) +from utils.trace_cleaner import trace_cleaner + import imaplib import email import re @@ -14,10 +16,12 @@ from playwright._impl._browser import Browser from playwright._impl._page import Page + master_email_address = "brighturing926+49492@gmail.com" master_app_password = "sijknxflhloopmta" dev_base_url = "https://dev.new.expensify.com:8082" + class EmailHandler: def __init__(self, email_address, password) -> Any: @@ -130,7 +134,6 @@ def login_user(p: PlaywrightContextManager, user_email: str) -> tuple[Browser, P page.locator('button[tabindex="0"]').click() - page.wait_for_timeout(5000) my_otp_code = "111111" @@ -148,10 +151,9 @@ def login_user(p: PlaywrightContextManager, user_email: str) -> tuple[Browser, P def test_enter_key_to_delete() -> None: with sync_playwright() as p: + browser, page = login_user(p, master_email_address) context = page.context - - context.tracing.start(screenshots=True, snapshots=True, sources=True) try: @@ -178,18 +180,17 @@ def test_enter_key_to_delete() -> None: page.wait_for_timeout(1000) page.locator("body").press("Enter") page.wait_for_timeout(1000) - + delete_model = page.get_by_role("button", name="Delete") - + assert not delete_model.is_visible(), "Enter key not working, Delete modal is still visible" - except Exception as e: - raise e finally: - - context.tracing.stop() - trace_path = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(path=trace_path) - from utils.trace_cleaner import trace_cleaner - trace_cleaner(trace_path) + trace_zip_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_zip_path) + trace_cleaner(trace_zip_path) browser.close() + + +if __name__ == "__main__": + test_enter_key_to_delete() \ No newline at end of file diff --git a/issues/49543_647/user_tool.py b/issues/49543_647/user_tool.py index f1b06a8..caf7aee 100644 --- a/issues/49543_647/user_tool.py +++ b/issues/49543_647/user_tool.py @@ -47,9 +47,7 @@ async def test_issue_49543() -> None: raise e finally: - - await context.tracing.stop() - await context.tracing.export(trace_path) + await context.tracing.stop(path=trace_path) trace_cleaner(trace_path) await close_browser(context, page, playwright) diff --git a/issues/49933_1062/user_tool.py b/issues/49933_1062/user_tool.py index 9265c26..2a70df0 100644 --- a/issues/49933_1062/user_tool.py +++ b/issues/49933_1062/user_tool.py @@ -266,9 +266,9 @@ async def run(playwright: Playwright) -> None: raise e finally: - await context.tracing.stop() + trace_path = "/app/expensify/user_tool/output_browser1.zip" - await context.tracing.export(path=trace_path) + await context.tracing.stop(path=trace_path) trace_cleaner(trace_path) await context.close() diff --git a/issues/49993_996/user_tool.py b/issues/49993_996/user_tool.py index 740a065..500bf86 100644 --- a/issues/49993_996/user_tool.py +++ b/issues/49993_996/user_tool.py @@ -3,8 +3,9 @@ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, base_path) -from playwright.sync_api import Playwright, sync_playwright, expect from utils.trace_cleaner import trace_cleaner +from playwright.sync_api import Playwright, sync_playwright, expect + def generate_random_email(): return f"bobexpensifytest+49993o@gmail.com" @@ -16,16 +17,12 @@ def test_track_expense_rate(playwright: Playwright) -> None: last_name = 's' browser = playwright.chromium.launch(channel="chrome", headless=True, slow_mo=1000, args=[ - "--disable-web-security", - "--disable-features=IsolateOrigins,site-per-process" - ]) - - context = browser.new_context(viewport={"width":1000,"height":550}) - + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process" + ]) + context = browser.new_context(viewport={"width": 1000, "height": 550}) context.tracing.start(screenshots=True, snapshots=True, sources=True) - page = context.new_page() - trace_path = "/app/expensify/user_tool/output_browser1.zip" try: page.goto("https://dev.new.expensify.com:8082/") @@ -34,15 +31,12 @@ def test_track_expense_rate(playwright: Playwright) -> None: page.locator('button[tabindex="0"]').click() page.wait_for_timeout(1000) - page.get_by_role("button", name="Join").click() - page.locator("text='Track and budget expenses'").click() page.get_by_role("button", name="Continue").click() page.wait_for_timeout(1000) - page.locator('input[name="fname"]').fill(first_name) page.locator('input[name="lname"]').fill(last_name) page.get_by_role("button", name="Continue").last.click() @@ -51,7 +45,7 @@ def test_track_expense_rate(playwright: Playwright) -> None: page.wait_for_timeout(500) page.locator('button[aria-label="My settings"]').click() page.wait_for_timeout(500) # Short delay - + page.get_by_test_id("InitialSettingsPage").get_by_label("Workspaces").get_by_text("Workspaces").click() page.get_by_label("New workspace").first.click() page.wait_for_timeout(500) # Short delay @@ -74,8 +68,7 @@ def test_track_expense_rate(playwright: Playwright) -> None: assert "ABC" in check_text, "The merchant 'ABC' is not in the check text." finally: - - context.tracing.stop() - context.tracing.export(path=trace_path) + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) trace_cleaner(trace_path) - browser.close() + browser.close() \ No newline at end of file diff --git a/issues/50577_1094/user_tool.py b/issues/50577_1094/user_tool.py index c66d6da..e030054 100644 --- a/issues/50577_1094/user_tool.py +++ b/issues/50577_1094/user_tool.py @@ -11,36 +11,34 @@ from email.header import decode_header from playwright.sync_api import sync_playwright, expect from playwright.sync_api._context_manager import PlaywrightContextManager -from utils.trace_cleaner import trace_cleaner # <--- Added for trace cleaning +from utils.trace_cleaner import trace_cleaner GMAIL_NAME = "nomandevtur+50577" GMAIL_APP_PASSWORD = "tpye xlsj axyq nltq" -def get_test_user_info(seed = None, first_name = None, last_name = None): +def get_test_user_info(seed=None, first_name=None, last_name=None): if first_name is None: first_name = GMAIL_NAME if last_name is None: last_name = "" - email = f"{GMAIL_NAME}+{seed}@gmail.com" if seed is not None else f"{GMAIL_NAME}@gmail.com" + email_addr = f"{GMAIL_NAME}+{seed}@gmail.com" if seed is not None else f"{GMAIL_NAME}@gmail.com" - return {"email": email, "password": GMAIL_APP_PASSWORD, "first_name": first_name, "last_name": last_name} + return {"email": email_addr, "password": GMAIL_APP_PASSWORD, "first_name": first_name, "last_name": last_name} def delete_email_inbox(user_email, password, retries=5, delay=10): imap = imaplib.IMAP4_SSL("imap.gmail.com") imap.login(user_email, password) - - imap.select("inbox") status, all_emails = imap.search(None, "ALL") if status == "OK": for email_id in all_emails[0].split(): - imap.store(email_id, "+FLAGS", "\\Deleted") + imap.store(email_id, "+FLAGS", "\Deleted") imap.expunge() @@ -93,7 +91,7 @@ def select_activity(page, first_name, last_name, activity_text): page.get_by_label(activity_text).click() - + page.get_by_role("textbox", name="First name").fill(first_name) page.get_by_role("textbox", name="Last name").fill(last_name) @@ -108,7 +106,6 @@ def login_user(page, user_info, activity_text="Track and budget expenses"): page.wait_for_load_state('load') try: - expect(page.get_by_label("Inbox")).to_be_visible(timeout=10000) return except: @@ -141,20 +138,18 @@ def get_context(p: PlaywrightContextManager, user_info, browser, is_phone_setup= """ permissions = ['clipboard-read', 'clipboard-write'] - data_dir = 'mobile_context' if is_phone_setup else 'desktop_context' data_dir += f"_{user_info['email']}" os.makedirs(data_dir, exist_ok=True) - context_options = { "permissions": permissions, - "storage_state": None + "storage_state": None } if is_phone_setup: context_options.update({ - "viewport": {"width": 390, "height": 844}, + "viewport": {"width": 390, "height": 844}, "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", }) @@ -163,6 +158,7 @@ def get_context(p: PlaywrightContextManager, user_info, browser, is_phone_setup= return context, page + def test_inconsistent_button_behavior(): with sync_playwright() as p: user_info = get_test_user_info(10) @@ -175,15 +171,12 @@ def test_inconsistent_button_behavior(): ]) context, page = get_context(p, user_info, browser) - - - + # Start tracing + context.tracing.start(screenshots=True, snapshots=True, sources=True) try: - context.tracing.start(title='scenario1', screenshots=True, snapshots=True, sources=True) login_user(page, user_info) - page.get_by_label("Start chat (Floating action)").click() page.get_by_label("Submit expense").nth(0).click() page.get_by_label("Manual").click() @@ -203,6 +196,7 @@ def test_inconsistent_button_behavior(): page.bring_to_front() page.get_by_role("button", name="Submit PKR").click() + page.wait_for_timeout(1000) page1.bring_to_front() page1.get_by_role("button", name="Submit PKR").click() @@ -211,16 +205,6 @@ def test_inconsistent_button_behavior(): text_locator = page1.get_by_text("Please select a participant.") expect(text_locator).to_be_visible() - finally: - - context.tracing.stop() - trace_path_1 = "/app/expensify/user_tool/output_browser1.zip" - context.tracing.export(trace_path_1) - trace_cleaner(trace_path_1) - - try: - context.tracing.start(title='scenario2', screenshots=True, snapshots=True, sources=True) - page.bring_to_front() page.get_by_label("Create").click() page.get_by_label("Assign task").click() @@ -240,16 +224,17 @@ def test_inconsistent_button_behavior(): page1.bring_to_front() page1.get_by_role("button", name="Confirm task").click() page.wait_for_timeout(1000) - + text_locator = page1.get_by_text("Please enter a title and select a share destination.") expect(text_locator).to_be_visible() + page.close() finally: + trace_path = "/app/expensify/user_tool/output_browser1.zip" + context.tracing.stop(path=trace_path) + trace_cleaner(trace_path) + browser.close() - context.tracing.stop() - trace_path_2 = "/app/expensify/user_tool/output_browser2.zip" - context.tracing.export(trace_path_2) - trace_cleaner(trace_path_2) - page.close() - browser.close() +if __name__ == '__main__': + test_inconsistent_button_behavior() diff --git a/issues/53036/commit_id.txt b/issues/53036/commit_id.txt new file mode 100644 index 0000000..2771951 --- /dev/null +++ b/issues/53036/commit_id.txt @@ -0,0 +1 @@ +04544cf48915328dd8366c3b245d06612cd46194 \ No newline at end of file diff --git a/issues/53036/flow.mitm b/issues/53036/flow.mitm new file mode 100644 index 0000000000000000000000000000000000000000..4631081085a3218c10357b1558ed174b858a72b3 GIT binary patch literal 295522 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

B#)5-M&TGmU=+NP!YCS}7>r^u zio+-#qXdk`F`B?=5~CqS8lxGEW-*$>Xda^ljKML6z(B`?p)dvvVlalq7!G52j1e#v z$5;YmNsOg1mc{^r%CZ>CVJwfa0>?fIl^5mL-yUSxMCJuZe zFazWg5=4<_;YI*;H}epvapvODGW50xCNG4b%T&(|s+)7oo#VJ=+N7K-^g7}4qy{6J zO_*qLVWPulvGkaZ_*>Awi0WW50HDQ)EH)b`!kJ*7)Z9dFu zhrhudK%ih&zX!8+n=o4&{J0#L&D)0A{8r2suwZtR7qdHjnB4`xJtoZVh2Oq5tQEf7 z+73VNZmiAh!`iIyWAkEdP7BuN?!?->@Y89++TguyJ$}p~wqXuP-_hp999|RV@HsGt zAAUOE2cB_4o}DHK=Cq2K6YOw0+c2lgk2xV1TZJ$#n}Nrop7(qg*l}m}Z<7`CL3#SD@EN}AgL?Nl%$Tp;hxs56K0o?#VZII{=Ie(0 zJr2xoGhqH!ALe)ZF+bF`AME#gyD>jJ>xc62_xP}YXu|^5E-V1)2D$@S0IqeI4e(=u zANU)-*AeKzI(>euOSE8JMibW64!_+NtlR9xdcejWD4!l@=iCesHFvesb(2&v)2sLw zp!4>Nwtgvp zIyEOQT_E_Z<+YfpeqkMxO)G3~FeHyJw{Tw?RwP@v^wOzN8fFc&MXt6jSW(2ao6sC( zOecXl1SS=EMvC|l=*N;s&jF7fY(LZ&?r4oW(lGf}m!;~Z^2V4WQ?vAj5{L~XV#%hH z5L>Klld6h$3s-uDLZ%lv6kX{GZVA@OTYS`kokDm((O|YF(xG6&n3Z5DQ&o0om`tZI zpi~)ap|G%uXLI1X9sT+iE@8#ts|=Y&nQIWO=&JI8&y9)oIHe}7&(OpJ&IC(3T*%1; zl;hn9s+WozV~XO}s4Ac^oQUP(VXqVch(dS{81&Fkqsqq7{sQNcXg6sBk3$@GqT@%* zEm)i2aT{;eq(wJR2_%7o^84*5cOK>)pFaD-GiqvSt$j;QuJsBRI{ z9$VKUR1BvJFlUz0G2Pr0swyigKDnwoMk^r%m+_a<78r)oSwwMz#n2`VNId}H8q7N4 zsR1kkzD42o)RW8y=?V`#q)pe}u2!8bVXpDAoYp@3B95%5T zN`8G|@bIQqXj14{sp3A3LbHz`S|0gCDTismQzX;G0r3hPMIm^A(wwBoh|5*4;RS|f zIgnvzC;>Q57!@ZmJg=%@ZBB|{O2_lHFcq+V(d6JvC=7=}F~#KqrIiL!qa+a1A#V)K zaT zpQngi7bcLldK+y)8MQZ4&(A#SmMTD!OQ4naBqVxMShz%P- zvp48+8gw}gx|{}GPJ=F|L6_5@%W2T%m`k<>T~4QS$QW+VItD{l2VM-t`(x2qsVNo> zlp1t7rIN8ReQshFo^0Ct;FgDz)iDDNoyld+u5*WTeXge~nUArUKb4ztlcD8#vB z)J7#UL$R#4%{OFg%Cx6@@U|{z*s!F}RwyP061G&n7`Ed{r_+Nc`?~wt!MxMr%MVxx zZ^me)?CyxYl<6>f1`E8kgKNt1h0s9CJDgiW8CwiVLhxGJ({XpkG*Bv*UD0CFL(p9V z<-x(WHYVM}5BHfUa=_yC3=9SNLXWS|)JF&X_N1jjm(!rjY0%{~=yFuil`Psf(S)100KUZx`-vzie!RbkkIz8n$XSfUgcEDxJ z;bSc*j2OIj^c2ofN)IIi#4S9$1lPkEF6f)22jUWhn{fJg3IYv+2`|DaO4(&HR|^OP zA4*!_zlc=SrKAbOR&W>ioJhF6c}Yzg96SvUonB>iengZYvSN(cbQ4Hm?5NCG1r&(^`_bdVD9j6?RF_Tn28K$e6&41 zJlJL<)8m+CMmczjJ$^$dZnVc-4u7i0FyL?`Vr_$Q3mLMTQoOnGhAb}`zPi!g$f6w_ssshutk6~(QBgNJpNvI7D4pxxcd+OpA} zSX-vgNyqYBGL`ZRt=_WD7<31G<`7GdD;bvLw+;@Tpu~+*HDD`Nz@Uvr)KL;ZUT^dJ z-7TQhjjcg^h5%y+20)6S=n)DY7{Oa0+`eV9RM{4$1#>u&==XL|<$5W3#*`1yVfO^>!`jE*YLlyyQbZ|^ z=wT|r60P#fZqiPKM$Nq`DpFsJlE#)}(Wr}3z{OJKH)r?ZM zaJYpgjkFPn8%0=BiL7YgL<`kCTK*3Jffs zaFfMkp-mz#5+sjXI3r7P9AUOtECLH}GSR@-LlH&`ZR89V0XRv`yuoBPh!zpIm_*uS zvKVOIAmEIM1jj{_$!rAX4&Dr$sTSUh)1t@-oKYa*d7h+s*2EbJ3wkL*p#{Al0_c`D z;H*&)cosK9%0?5%Qf8bn;=Bncjzx<}umBeb%hQz6OyOpWK=T~Qf|M6);aS?uG8Wb% zSb&$yXkfquvnUEIX%I+?$7#|GWx`mDM%+x&xBzcA@L&_iz`I$KXy$nfZiJ#TiWCIj z5hf^03nM}z6e~h0nk+DalDwG)3jiHWm>Ak(WC)%j8AjjvSM;aj}LLJ<~|Q842;1NBAWMgdA!u)r;XG13&=VofAVfTt4c41#Y79M}R>Yn;&l*%3`Nn2WvxEd%};ag;5F7NB8p@I^?I5lv88 zkcx?fYP2wB!3gYbP!6oXLCwN--efcwIA|Y?8FE4yX^Ut!m`xUFP$t0u&CCqV3Bc8? zfin?+wl)i#MV$NoF$*w`+7Y0BFpk<3V2nY$!A5ywBzN=~Fj8t0+BnpY+EizW`ca$A ztN~zZ6H*B;nwZ=qcoC;30ZKFpUIapouuf^9i&UbECKh`GU9`o~MI-C-2D+$$E^45Q z8t9@1x~PFJYM_f6=%NO?$Yygk&_&6iL@4PhJCZ`Vr_7hkir!YNY2qN+Ko^%F>YDni88}6GB6PA%$qWO3Bu9lEcjiHbiSDD5(Jx@6*|ah&{YQJzlaBK0yvkL z$+i^zIMMAk(`?a|GdW^yOX#qJ^=F5TEkU|7oU|}LZ>vD@!`Wg>q|j<}bb0Ip1*V8E zX)>4E%S;1Z)Ib+C&_$}yMMQ|BB6tYbN(f>^N2sti7-Vp5h{eNUMt~y*IB^H1F`$Zt zU+5GR6x>F{&`5BoEnGvu8OgNpB90|C5?;jU;4A@r@#IWLmGB@7oFp7q^Qe-Wtr(Y! zy!Dn_=<#A)w`}*AYB4TWV+HoP`jJ3@`kCa%B=sEV#BB*6kZk=@jWal;ZHRF}m(U_Q zh;$0USyE|`5R67azYkPz7yt#xjB(i!K`j6TDAHTnQlFI4hO_Bht^ov62?U~XE=&ez z#;wg|bLF5~t6+B|!|Qu{!_nSQa#;+{M#*9*(o5JR#_u&qK5LNa$am`aR5;OJEDYrB zLy;c8pD)D5;4bVa5Gct=L*8P6VXf^62bYd!Y@$EzkK1}3C3}0QZ+JK#>M{?P++|ma zwzc*QS%{&o6l)qDV6*L(LOA9ZJB9;!yc>?MQywzliH4+Bsctc@N-5Ql(X&_8xOPL-QZ7`5Mokc?`J7|e8p^{&u zvl&KLbXbj4mS8wf#OWDVG6b`A0D(d@aC3}kw$u#-BH)w>p+dJVF|I1)4~K}m@TsTM zD%CEI=NOKvIhztV$I~^DKkk&Z#FsnG1&h~IKdv*Ke` zHz$EGIT({b8RnRZM$jZGm^oFe04z}*{GVHs_Cv$sO|J+7EPB(o3cxr_qF&r6wF6tk|qZ&a7Cm zSu)Mj14iRtI%sk7b?t!Ri={C_E0v6=fx-KrH^)UMgdR%}sIM^y}D zag8Z;E4F}rvl6b@0`|?Na>W)ff3(u)P`X0p#D_Zc2*v&Eq${>axhWbOGiXhq&c|E`9F z#b!2RYtyh0YU|KXC!{oKju7>W7ALg?uOWCn&FgVoM{`W`j4}_j z4j1rRE(W=1+>pUw7AaCxXa~TC@k;SE3kD&0z_d~pPHyMI5NHdDGO|Na#K`62VGUA8 z-2(PZI-&z@v?x&Oeb?lLn@}+ckz^l+jc#yz-)jT-|x?L(QQ}_maed!XC<4flxdn!|?*2PlS)u*$^ z6)eU<%Lp|g&{fH%3sZD*bb=05(4r{rwnXCzP^Zn+*L-Upth(V9(A|;RjZEB;TcPO# zDI7FWXy7y_g(Sg{;%*2|h9|U*)y?r-M=;?kBogr)SSFWB9laXDwrlljm(nU$0H_Ld zC1YsNn_bu5xa6ms9yg~F53M8A&EZJCr~|~op+4QvTIOJ}iA<^4c%~~aW$x*nIPX+- z@=a{tR;2|5?Dupnb*0Mj@YD`-r65h&U=%|6lSqkua-nXtupqG7&@&ufPi-AYK7sE0 zc!|DJJdqF+pc@a8hrxtFg1JUQZ4#8Xv3Sm%&ZGe`J0~?yu{BMeGz%W?lY-fB-{CNv zB*6estOD6kBvhq=VXq+RG|CjDXm4mhdl#)|#;+ynnF%Sb=Eyc(S3#bkNIaK}=Ro?` zolQpy;k-W!y5(pH9AC6Kki7B3T?|tTStgcygPmndu(M2soos!EPhrXION@rIJDD^wK`MD>f(% z4fXXj}Dd%uxk3L^aL2{;A@eRlZEES3y*r@7l`_ZZzv z3`KIlRtkGs+Fh3Z_70Y#6HU?Nus0Yqbqg_Mpj?fY(Js9dwvs@%v zUea$GY{AQ=R>ADFc6z8nrmIZ2Tw=b-+7{{-ZQWwJ;A~^eUBzOM3`P6<^IkGS1q8|1 zN&5msIWJku!{$OqgPf&7&e9-fX^^up8ApSh#V2^<1zRy5?in;l5l_T!VVO)%p)b=U znZi~>*b(mz<~-S!202UG({E^yv(U>Ft#h->q%oSke!FGdvKkx*`g|gXxGJ&*X#t~` zve|SN)NGD}Blvtan960+**w4z)A@8bod8jrd^od|)W;)BL6#*#hZ!kI5ZVYq@LHM; z2_Ui~(OQz^BasLmrim!Ol-DCl`V@peFC~GO0aPmlQb(}7j?l4789gv)^Tfgxygnn1 zP;tmQE<}Tzr9sXz(jrJbrPu3;N^`*%5_;ul#hP&~cBEC}lti#pgakGgDx#&S2&WAa z5lS23_#lwFvvh=|W+G<+ttDz2?I((1C^h6P;~X7~4zbY_%*@;@K!i>+Y;73{pfth9 zX+OQ*HIRPow|jrR5eEmVL*Y5xC zy%(Kl`S{V7-dpkhcCT-D%7T?E&-?7M)kF5D?$WII)j#h0$I4e%9nXB%;63)BtJmDM z@`Ycl*c;n!fAWDt*{;J{*X`l)p4{`&#rU;>X2QDYknOA|gwMo&Ie6wA3XC%JDM+B zxZi>kF8Kaw-#Kpg%)|5AALRX)9CJ|H0^@`8PCV`6GcRD8jyluM|Crh7jcdf?qNlrW z>NYKW^3?5qe$%a2xEIiSynV&0qmDewVYqI-C-K{pAKPz_)b%ak@ zc+h!={Fyk^7W@uzrE#DO6W-bNo6vcgT^{>u>){K2960lu!{=Xj+U=oJPmI#%F0h`n z*f{J3w?9`BsCy)}3->ACudy-xl6#iuMX+4egbZ`;24{*LoB z>lVGRYyP~GPO%*r-(9=?!k?ab@n5glal`4FRr_4M%^zAH{L_&c>5_dkZ?AQJd{ZvD z{=W|%aphlreb;?&y?ul3f$MMn;P`h$c1^XlT)Z+~y?-OXoi_p{Ug z@z{qC7T(i;e9+C`dTvF}zja;5e*3oIf9_iK)+Kk|{D%kE&i`)Tn}1Ed^JG_Cd-9&& ztp359&ge^}5-xu0eUi*=Uwof8>4%q{ zvSFL%Rj>WgeBavNhulZ*@SOt(%G+PCXuk!&e)8d$uKdRt+igg(&Uvr?<;ti2(|XZ| z7yi2C%}clk?p$Bio$%+&uYdFId+zz*(ibkcdH*}@=UyZnd-;#{`Z|5>eaRQEO|3of z)UMaAHotfFP4>6GdHwGGw?E6SyioVVb5G6tWR>rapFQ>DSJ@YmfBMZ&zxvtN-+Sug z%k_VF^1|zmz9aV0i)Y=~^Wv9ZZn$RuXO^uy{Fax0yYz=2Kd?6!{Y(EN&%FPwvtKCO z_pc|Odf|-Q|M9>9|M|y*XVL7MZ3kM8`|_RgGq0!b89e6r6IT4~kOQ0T-?_oI_zaBs zZ|tXSzu7Z?kMqsas)LWyoI3pPbw0<5mCeI{VvSbiDP$-lq>Ze(9^}USvu~(mXTzl#5{m%t1dhz*vjkmme$s6+b8hyZM+8g!iwuEZcM2`~Q6NkN$l2C-0tn+#$?{!}h%SJ#T!+1D}bto~7Nr+x^K- zKY8WY^jBu@0p-=E{_p?Rz5mJs4m`-QLDziMnP*>9JmGK4uK(GhmxxR4s`*(D{L_K!SVY}Y^`FV?5KY#1RN3)Jy#P5H!_*XYAIq<;Ud>=fp z;jXohetOBlH-56}V()La+y1UX?&w`^)7`cH#s3J)zvVjO6Zho@AKiuD^c;K8AAk4q zKAu1P@oitzvg@zNi{e0P)=ARyP#6ZVW2RnN{{`nWZ=e=;+ zRT~8B8y{a?Sp0f>+Zn#5^><&m{qeu%KYgR~yI0>_IO6{A`Fj2>J+FIjxA(q0gM;y- zg?#h$!}c?s_{3#vfA$c0@0u$=d{2Dyq03J^W9{+OEhq1@YUz)+{W!6|ulLQ@KL5ob z#~!x+mG^&rv3ZZgrFXA-edyowmTr6CwI4nAy&oL@?oIaeryVzaHUF?1Zu|3Jj++1S z)nDAc;K*gZ=ihwln++Sb;d=i2?pddN_OScb|L*YFEB9Xe-&^-sm00!bwcmd6&dXm9 zhA;ok69@n3wTId+{~mP>VLbAhCBti;Jk|96x*z@C`oKd!*f#x(SI=7aorxla070!s zq)_Gonguo4)DjD54sVc?<+Ny{0Z2k%2g5))6IAN}HY6>RqoA-AKqdtW6}fyCK&sl& z&51UL7?$GsGDg$-Byg+&6U{P;05uvM$Cqm2z$I0PNWMbIluiOvCI?DySt*eYM&wE( zN&OH=LZ}{79;gXLz5&~pkyirvtQEfOF?vu2N{HZFHLK+WCB(oKfll7Q;``S{)&OzSeG7sCbxBm#|Cx3?S~N5M5pPDZq|@h(?p$V%~DZl1Tstp1Rhv6yuOv;!ZE4Rt2%l%>@u z(xTTy2h5^p*c2^gyIsCsZ$~=Xk+k``-I=t@*x40Lwe@v!&SZ>YOZBQ0Zc=ixEZ1f% zXYGSSJ|;=y-o8K=GfZ}|)S#!kWQ_&8Q#n!B8?mR7ow;!(LkQefp;8#8LLtOUNo(6| z_PWW*D2f5mT7{bobPZ*dLIo(eH7OJF6h*_Ha>^~`xUDCx<4G0-F>CUZA?31~^kg|c zBZXuAp;)NPnH!E8>ZK!fJ+Dszd>w0!BaFY2gkFBVzVdR7 z@@hT2SfjjF);E^_mS3r_yihI&{b(Ve;F~Z^K1yq{p+eHrBuLtE;DW%K)6pnIMKtU2 zHFIBDQ)mmyfi82O5{ej+vOe$wN_mJNLRx}#D^sd%3lrrak%M#Yb5@Nn+%#!4L>L<) zjQ*(Qh=0czaAN*$raM z?lai^T~xH4?I?IsZNa!?joG5DE#X$Dn6?^?gI1rD77U`(*&vD?()@wAysgtMp1lM0s&79{Z`bs2D{Cs#6hK7$T2`{_+ zV%g~rl$|CI=`wkPm1~64-(9)p3OWt3Axpm)P!e?dO`RQdQ`p*32$Kn*duRGXBwp(D zi+)2aJlHqT-|qI<4Te~(^0(7qD7lQH=<_siz73pj1Lxbo`8IIA4V-TS=i9*fng((W zoG;Oq^bC4pcxwyMYT>;JOR+VWBRazUj$%`Hq8Kocc{Y`4XB#-*V%df_aK3pNw=C#K zd8m2;+T07rCxy&XfHQ->kRaeJ!3jE?CTN1^R|3Xh>|rSt8TS~86FQ1y8H%T-JWm_< zK3*rV0#D-HN=grMW=jc9Un-S!ql61OU^ZC_z-MI5hzKC3_d9$=NCMpm$1`v|(hKZJ zC<`mt%7vu>PJpL+MYFG$WY}J-(b?;>i4;$xF9j2sR4*J40WZ^1g4L7yJ`&?)t-%&T z2TFIe`~+aN;X2$81JO!68V|!k*Jwf;h5ZgPVTNS|Z6rj11R|U#YC|+jYMBTlktD@X zp=h)U`%P3ZytJOj`Ed~5@yPF4;e6qwlVkXxq$Q)#ke22IUMr9itHp7K5<+;85@@8p zIIRey0M$GV&X?GD5k^*ru*9IkI72HE(1Qd;&`ix^*;$J)610o35#vM{FWUa$cU6lp zat#s2_Ee$_zC#>|+6`&ZX)_>UB#hFNIKs-n89Q{e3YcgVB}40D!|@FGZsprU+9A*| zY~Y+)N|Fv3BCAJ9&7^TtgTzyF`j(cIG+{)A0tNIDrB|RwsMNFR1P0@LDy_hgFb({b z%crv-w+BEasI`igBml$!>;XZ_~(Anqq)F>jYMkM~8=V`v_3 z6z3#GsrHDoWGyiLem~t^F!QBYn=|JeN_G~b!}*SI%*)u@+wB3fxi6P9W-Sh9is;We z?X4`6&vujTbeQdrNlF!7A6Ed&n7g{=YSL=zxT9GNxF2__&I!nR@&Mn=am zpqV-MY`W#nnLUpJx&cr1* z!b3h@WuPxuO<{^4EdYa>J;NPhXv3z`hOF1Bd~nD-BqcL*WDl*@p7HoBDObg_M-UL4 zMF5x%me>TtQUd5`6Bt3mijSi~D-=Mp4B)s3IPAh;i2zqwj^(R34ON%#4EdRgOIKlt z4CJJ&J?D^sNj4SprqjtyLleo>WCL}}Xs{I;F`q}(i^vOs%QjaC<-*x`NJ1aNeLz7zRwqrNM_h%hV!|} z_KuQ?qfA5j%Dc7MY~vHqe(cwnExB@8IdbT%wiNP#QX zH_IW-b5J<5ArGIjASx)-X-anEtW-yaJcX$nXU25nI)Gk5_Zo3Xr&Ga%I}JSJ@A>K{s-|2{%z<)$F2RrYwLvRtgM@M=;b~ zFxcow7;05mx|D)Y4f+3KSk9H*gaIiyfRv4eED-RA%UG}o1oPQ|*MY%kn@E=>$pMZg znPL}JgLw#)0CJKMCVq1&2;{pF46#E4DKEH--~)8j3X8wpB&JeOa$%|ZVk8M+H5d$U zX|EJdh6>pn(7dMd7;Nv1;BEqzqPUbBc{g122V>(un!uVMywnTNj=v7A#T1v46EAlH z8+93?`JDPNkh-!AJx)pdkDR!KuwGigHgc=-->4vs7;_&nZqDB3r*ENDG?nhF$|f3cFUF4R35zm z-@>w}dgi7l&!8VY^;m+I5uHeSBVW~dq1VObvM>Y_sC8rGWK*DzD(aT`k(-r|zPyw{ zr2=&XfmTw)FJ+Uc#BvsRnG|VHMdOh)>bc;nh5}SRd!-x{B9sXVZ-c9fF>)KiP*4I4 z6>%6O(Erc^2rJ3PD)-0zZBEIPDC)V;^Wy2q=!2;NDKa*?jC^I>RX+?xb5R2{SyRSi zGEeuIB*oz%Kp@MSC=}h1x#8%hH-?4XDdmGm*JJY18aibLx#Fdq9@xVj5Qhj=W`|A` z4dkDVph~--qViHTf})c--+)w_iwb6lniV{{dbYsm z31LdIg3^eX2!s!7U{p#@xA_?xh+tcT&IF7w<2K7j0ub8;6KD>_Jn5SSg2tI3#YTBJ z4nhHlzE)#BTTX_YRxBWpLBVUzU<%yYXwNyKFjSDQYtS-LuXxclCD_nuP}f$zV>jtF z_%yq^aT%*)XTXp(m*SHKZ!gkTfN@U)L!qK7Gg{ywDIA0;RRh{GrJ(F>ffXFc9e{rT zCdFJX3d`ZLZj#lNf^OE=lyZ_~mf*JHERbtt3O|8zcbr}oq_i2eu0TmE6Za~O2F!sKCENGEeNG4^MxrZJM=M?&RbxW3a>)pl55r=58#Xm)SN8GCp0=6 zPBmAKXbP=a0j>a8DiyL){rQ{Qsv3du3*j8zK0=D=8G__PCa zRVGv(b=1zoX4LJX>lv2?RV@Q3Znl^*7RT2*rUS2cF`h2uG;q#1GH%1HRXuB<)zqW~ zE*A;$VlFIE0Ct1p02(Gah;4;= z9H*=1%AoARmtDbFXv9|@hNB4N5r$u~e^&mZkPLy(7NK?F_@s;>2PZmG31=m^1cx#s zhdV$|Bgtn%a0CNxMYsj}&akSBpaTvJwsWW!^BBxA@@bB8hA{fkSb5#ZZR5ymM=m%= zUI5JqNlGUwFBtha`UTiafPH?%7_Afr;noP}4mz@d$i$#~sXqzg*VNt#T_UR6WpNz(io{8As(a#y_NQ zUcFrA2U^~&U)~I8KXk^nyct_wp&*hkgCkzKB-0Sdmyj=K^qR~Yq+Evc6!~(6jtDNw zd_>E~5faG{%I=;@a`jOX$*)B@f$xuHWjW03)wD!#dyJQ8d4-q=F5+wInM3*74!Bt> zbdQ!l%Iv6zq>gcCCL>0DXZ5RdZwgzKEzwk)w~Cvzvdi~u8Zm61s5z5wf!#GMC2MWB zM%NXy5`nJNSUt_9cGA=BT4y-3$jSP$b1|e%Q#8}rw|Nf)c&6sKyA>HZ{SIV>X6mEg z8EP7NoU7_t?Ji!Bm9?jlyH$%JRUF1{#Y1O@B}OC;^G`yd&y%gK&6u`vY1S=wt2P;B z4wR7|xjq=!JCI{Aoszlds_q?Al0qvWBuvK z9;}Jw-r&L7!aP_gN=0C4j&AT^RYIubxN8XAh1lL1LVP%gyXGiL;OYOb;;u;n1%gsc zCG^N(w%T2jg}E9$SglPSQ>Mf1Ge^ukBZfHE(U%N*`9NDSSu{Gc!%|nw6!a8fdZnCM z0PC3y<|5wB5Jgu@g7fBDlT1@9U0Bkd=sr1)d_Q+Qc0yc(Ck*9YNBbo%dI>6oE;! zC>8^8QbELOh1lwUl?RKAQd^bYACv1A!oiHWt01NeLvcT4^Eu)jp)i%r4N*B;hd=D{ z#PZ11qkX5odwNS`j!AM9}V$IPB?Q5Q0b=I)Ze zE#k;TyNkH9n2B13O{|^f3nrq=7LP|+htJvB(<=;`vr%!Y(EA(UdP4a@BIxw=Cj+)Z zES2eVwE2dMv4q%3b*B5J;ZoVv#^gGp-D5mhtRT=^2M-nvZ6dAX54e*48ijbrNjyv@lX<1u%2)UfRE^- z410u+2=rL_EJGgAC8WMs2S!U^RTcMWe7h8JgV}RZc$_LrHYXa30pjDyAm?Brn_r*o`&{SR~GYi18 z2o|Np>7p=cMOHHj5rTkPhUNpLEx|-QqJaoh6ntG_1(TUXWbnEMeO<3PoMib(lp%;9 ziPO;#zFfz z3&>{R7;VwDq#$;?7tqg$RHS7TCj<)7$z(d!0#qjXmdb1Tb9w>h2@S){M+Q^rR2hKi zxueEC6oxLT=yRG8{fZGjhM;tEb!MkJd6FLY=2;ZVxVH$Lo*VZPSXZYMUGQE7-=6KA z)HZbzu;$dVx2j&1vI72Ir9z=4R8rQW2zjJ$oh{2PP6w4t@f@S4s;U_gY|MQ^3{KE` zlAJo%W4YaI*{t-*xz8vK((HN)+DP-vBRr!cSTtl!1AbJybl4d1G(Z~~pbZVsh6ZQ@``ExfHn5Kk>|+D_ z*uXwEu#XMwV*~r>qy`$;$9ykx|1V{5(A>oJz??W#Mn^Q#Qcj1j@hooDVot5@qrB%f?`Q7Fs_5RzvA zJ;$@CYXjvCoV2C~;u0vt<5tQplL2!84LFpvz<&{`sGFn-33owUmW10g zD=f;^FV)P6!eT=Zv;l-H}g zGTTd8R91=tVgq7aR3HhrmyEM;lbg(BW-=|A28{70xyfWElg#8M({${hi@G*o@xi)^ zZNY*C%i2I$dDwN;C!(%m!zaap$lFEno$n^oC({^KcfJ08{G8nOov)wood5Zs9JbUN z4+u3`@g}>^YHDgB^RfhmwYfd@4Yoi1ug)y9&paX=3H3d zavPusK!d?St)l(`D11AF~aM$erVqUlqa-Femo5H=PF0dTrgVt3fC;nR?ny~}en z%cS3Dah@g8f+Ax|IjuBDa}-L*_7>!n4m34r5*X)*KyogXI42!fUmzB(HAfXGxpoIvGU||J2Mq{kV3N{RsNGk^EIxH*)IXeUkCBStYv)ehy z`za7S4f+Nq+C@7h3YdTs9A;;&48gLv&2G2z417tXNEUd#t#-=FTI@VYFgDI2+AM-y z!0e(xiK5*?aTehC5+H9VC5WQUO3)N%gY%A^vtg7V&^&A933#3(D2@?XD{e<0#X-Tv zC|+O~nzCSwmFGDIvq8yLk!465Mq4pX#4Uti7kN8PLFQ44wAx6_W=FGxU;u~1*g1x> zF|?hr^LEH(YPHZHf=v*3hOqDugo9Cp4cdgZTdkOlpfDc3Zs9;CmWFRLqG01VI|itB z_(uvh5f`B??X&=ekcHhF4C0U3h7uN6bQidxWMoh-X@TC%El2Q z!C5ifVuO|-Db~i=a4V17FdGf>;Z_V}=;J@r#r|3Z4MxNtH6tP60Qjt5B&&# zT5+DXVJy$tt-z&jWkieBB4B_Pgts`moy6^;mA7FS4gE!8RvucHx5F)*wo)YAVnhNs z<3*YT>0ofciv(@4LPK+q-_-_h0na?cL9%4lYJutqBJfYx&^s_P;6E#dszp;g3=9@t zgfeMCgwBFeL;||ePTP1ZoI9W$7+7iS0$k@rtHlB&v;=K~nvhn?F4!zK(GCMjg+TJxF%@Bb!-J4wl()`3g8l?3yED)_~Bw`wgm|X+XXh0ebNTUI1 zG$4%zq|tyh8jwZ<(r7>$4M?K_X+jXzs{v^|g3aXtBdRCM>h!p>1uz?D0HQ+**F`+h z000dD0Ku4vUmTEzmA$ebXBndpEdNG|;8>&N=em4RrUK7celGaNV?o*}UT;b>TzVaRhiNU988Gt{k^`9c(D1kwHxa0X&j zS0K&Vvu4$GKxYt=#`@8jD^~bN-*Mv9Pj*hZ>z{X?IQ6VMN6E`BE*^X6?>Xii+uFr< ztbK0g&8GQ6u65F=H^dvM%S2`0;o)5&#Gk80R!_%6}d>g*StT+ZQnUfYL!WCh=&U;U_>v1}=9w1~b$Ez$#xj zn(zkzC@F*08{L9effT|3V}r*jjFzYXCb6ua^O1u>2jB$2wA#=CY!V$%2sy3iNj@K> zfsX^sC6VL{IUI-pY0k187D51Uqn!`}3>ZsUvR8;ew+F!u+y@p9uz($?Ii3aX2Iv)_ zfK$5!&Ba(W4Aricnv6hgl8ODo91a9+hMg3M@Ql7HV?r_}iEa#pc^J#v3Af`K$I!IqPdxiG#9rxi>pHR3cM7& z_zp2Eud38kmQ|C(2(>M>j)M9~aX67h6eUDUA!zZEULB9^@XuKYhKLe zsp4rqXsG0w4skis48kTFLjKG^smGlkZYaZB%DJ+HT<*xt;Z5cFEqs=P_bL`Dzp5}) zo{vlRa-oy7z&g?sh^6k29DvG0DtZZUNr_C{pbHr)D{;^QLC^8DX*rxZ+wCr#OPDZ2 zD`Q6NIEgxyX_xJUHA*FD&A_`YB{)rB#F6&Tux8TS5R!V*|N81YfUN~_JBh_`CWgT| zise8o$?uN=1ycqf6HOSN^LcvxxsY6r9mzRF@4 zAV<#a1E{6ucmaw%0`F%PMlzB-fpLtM;svsj)MvSz6mJonqEmDt5l%{!xcXEg7O=AF^J zGn#ki=i{C6GH_6nXv`>MeuygdF_O{CQeI;K<9#&45`IA7L$t9H_0>ecGLZq+0B|4R zJOMnFoC!BE5a9w4dWJuv5>AyeVwOcusrFeg$C_}~L{Ov&$GNsMRV%!o1i-d=XVh58 z!w7TkdK$wagoo?pG}eAVRZn9Czz4D3jbdFs$B-0()BjedG0a>^n&GgD+!v5^`*bi0 zAp%$Rl&ty(A_|H<@<5BnqrPYuIwtO`1E)sFpFx5zi1bmkmzInqj)7;yk46L|$wnWf z@Z?A@59c1N%ZR|?6mB--P#oBiurqLT@?V$nK%|~Y4i>KCq1H~)wMoEm5IR6hej|Zn z0VCyQa3dE03vPfR{d@rBK&7#MeKX*B;LX5vi$<=jW(eZIAOvXp%`eDp)3KBs zVSZZ8Z4&}&9oFp5b2Z>565+9Yp}HzL<_SZ-4bamo!Dp#n$N>EVo;pGxE`mq?TTtth zk^dH3mLp$hwH3P^T2PJVw#nDrHk#W;bK7Wc8_jK_xotGJjpnw|+%}rqrgKn@=C=7c zh1C4?ZX1&48E=4)AesiljexSJj6CM`8D-f=GJ${(4-m*n)K^Fi4&Z#Q55Emb15Mo! z95!&{X#Muki zuXW#KJdZ*eLrL_qQ|e=_u1X+Z!5W+eMe7pSZ;YnOfKCNKZ+)r#Luc1)_-kaJAYDWTcd z<=&%QJ!hSCn`1Qo-gT4tS57Ke{Opp2OYspik6k*=c7p#n<)k_OYyNuagO{I?yLS04 zr!e+e*PAaJJz<6+zoNSC(i3jjl5>oHL)})cW_4`KbGO*_C;efmW9qMGmWH8bxz=@R2CZ269r zcUrI!C!TP*bNB6IK6s(L$QK@aSH;WAKYr7EgIPHCnn_nIzwVSdH~nUNj_Hp}|M>eG zPnwiJDeo_OYWbGJ!gpfF`JcaM%)#<~9p47`?>WHxECKQ0n^#`= z+uxsZ-s9n|7b{a+_di-!?FrUy3@yoZ9JT7|FY4s$GWKu2^ZCmzpK)>g#^tw7ICfRR z!7tbRPh+bqc*nj`e|~-AoPX}9kIbt)Ci3CM<6n9IIP1IXP3L-^eC7GFa}Uh?$EWAt z^uYxg6@Penr6=LcT>a*Sk2O`g-+B1Ux#zx`vEz&bW$%=aUmtjOrf^nm>p1ggwhvak zv*+MF-&}FXJod*ED=)o!>3uV%zj#!pd(GnS>#XlAn!5kR5f`tUWN@D_Pj1rvIDN_8 zXYJYh!{clB-S)tj(@)MIr!O$)W(IeRn)J+;M`m5Mf6q+voD)yucRsc_o*lfA4ScwL zGxl}E$ZsyXe;>DQ#*V3pXI}KmxbZLj3)cUd@Mx_rLQ>n_a2|T>uS;`(f5(#h z?ws=9pI6*(!A}m%7}Ih)6|}_e5p<8^3#Q^x~5#+eM`Z_nGcK7|xvR%-l8c-X)Q5CmmS$ zgEVr-N439RuzPjsw{yQfbn}!E6N8D>Z+RNaE4P)DTJK9t$Ss++_fMJAb9Ob8mmhQM z%%?w_wejJTSN^5xmAn&QI`lENp>Xl0t4T7^@PUyFKYMOlv}|+uku}BlPkZD%?6OMV zQ(wy$UZ^{&@YL^K-!*s2=E=`YwEph${0VoS@!bWN-1FIWo5mcKSDN8z+4@%Pl^1{G za(%YYu<0A+shf_;xa1iR`~IFwx8Aw^#2eq=xomrZu>W++yo+Me{9_Jb;*rW%ND=-vX|a|;;qxdJ@{_^6M$LBR4{pV5d zpE>&b*-H;i+OT@Yme#HJ?NqMFD$AU*_vqrCr`~UzckwxQO(v|lm(DLeXT{BPD}z4- zzL@vatDD~AZ~wIXtsk2Ah2FT}*P~ZTTZjpBp4hZ0U;o5wdw%uGL%XIneK6yzy=R@a znwjQaH+?~Li- zf2f@H-nOepo#x)@TEFVi4HXakMyz}FhE?yr@#Q6tOxyRxs_z~ggqQ2wNtm8 zbKE7rS-ENAjMCREcRe|ly=TR~YXf(ep7%`AgdJB$%cR|_pMLf6_Zshh@4&7zRtDEq z&OUA2%VX!Z*QOW(zeOx&Q#bRW^B0Z-MP28-JS~?N83Kqw+Kf+ZMo$#^N8)YkGkUM z6P{bTbw>P{=l4CaWa%ZQe~xyXGWq(k=WX7R`TfFM&)l%Yv3$=3-#t0IW$|-MznHXE z-gM}jzrT~YWbgPH)`LgCHshwRvjX35lgq zWp5od_qUU~vbRG#$B_v!RfDhET>q&MQ-C$Wp0Dw@D@Ft!N+zf_K;R4Td86R8Om7&_ zHW2NlX7p6zv5+rrgn;BE;K&HEaS243Y@n$2C_xej(oqeG0r09+03}2$MWV?#SA!1z zQsPk*#R{>O5WJcKlt7SO1TqJN6AGkqPemA_V9Y*4WCzaB?v)vy`WZ2_X9YzrU@GzE zSSh!)%$7w~RaXbSCA8wmE|y&3dM?t?>sqr-DRY`w3g755HoeU==*3$9aG+vpBAAZNJv#o1I^cN4veq0KK`s72XUXYWrjz z&1(J3soLq1bpx7}ur66IHMxxXw^}DNs*z45zHbu+vD~A-)vp`$Y8RQ?`v=67eX!~$ za}i{t{xh>2Fl^J1^v9qFDi;WN)tj-_;qS;^sOlSG8<&T_q`HIxC@Sw%se#%z-TzQ0!(dVh47Ci6>0e{||0OW}4|RSFg6V&# zMGTzjf2bu4n(2S21sxvK|4>UEB-8&;iy1W2|4<7WBGZ5O;s(z2kJLqoTp-MFj_lx~ zX&aXiwV4dtvuv{j4b3^gD3`Rpl7*6ZX&dqi^^9;Ihs7BUh z$-bKCEL|xmZUB3ni*xX0mlTjnG10`#qWDS(8fs%hdpkzPgqh$%ljeekeF;a||H}bMf z83}^(`~8@Y!UF^0lHu^f;F5v!6b9_3(WYh(LL@+xWlRnxfC(BXOKL;$TAvK5k%1aL z9kXZG^B4sQT)6>|$uQu{?43-e)A!&!#_$}0vHw=*vBPAO@k=!D2r<0T@5O;%h#@(n zmjcFp+7GT@f}}}rAaHnWGMy=BdSjEp8D7FzA8!nB6l0`lf;UQ7fHDFpwoC_@0PW|P z{$R;0jDtR4$>5^G4jYzCFa*Xk{igEfO$5TrP-z{;KzZDONxr)jQTr<$O#icUj0UIC z^mV-G!Quxg3dfuj?Qjy1_L#+9ANO6XbltCxn)a)`qsXpA{y2>>1AN@z}z6knhhPa=j}x42%|Mj5G+VTPW$){Xpub@ zGU#wd2hbuNfswQhwlA1Y!#Z36v0=`O@>VXfYy})i*}$Ik;D* zbL~Ff48G9+FB{i%x#;o89EojC&CmQ2@mPReFY|^LFv>oI0VOFCJebtj0{FWM2d=m z>}QebSJXj6uqfG!;e&=?K{|He(obQq_#^}?FB<$g;jaMI1f)n{N~b$9xC1H&w7*WA z@Iar$V;GB(Y})M0(O@Ak_X9R)+}Lqr7oM*NHhTr!eSm>l7>Wdu#C@DbI7xlq-h6j`cBv(A*OWPj(f^1jsP6{`^HM!f%n@yr>PsxJYkU zltMA8mjUgUyrd!-)k|nVw_j6{f62>${!=ff5RGs#0+Pe$Iv|DWgX)+c6sS=BEUF2- z-%*uCJ>Y|In<5ArR!NeCa+3f;xQMkv>vefC7gq~U74W9>(bOzL^1f&Li z2l`FD0K+8^ia={Mi)zcmsuYFk@08~B%(V`LU5=cXH2x!XSW*0H~dpOFjHPjBmXnu0udlOA9N&24%4k<)FGu(I3I>Jte0(lGLUAW2E0(Tuf?f#0IeC&KMA?} z3NWz#NLw(fZ>0`YW3{rd+61C89gI%1uIJ!HJp}7kJR~b-9a>oo$swwPIccqe6iKq* ztV1BvZtXQN5$SYolN3>%IX#3-W}eNQic?pI==i}WK<{7P=q9ze*+5>Dt~#Ur0%c2IC%- z(qb9WIxAf|aYzG=G2vvN3Pq(Z-9Yn8;Vwg~5x`^uq@#?(pp(KD8FpW}G^B)JH8X^i z!e|{dXl!^SPS<`z)^EZ1>!-W}*TWc~V(oM2|HS%jU-BJBBawd0Ke$F>`&u7<294CG zd;=f*u>U1T`f!YI$R5^LtM$G+9ijDsHSt8+fey!_N}UyS0$3>FZxfKLch`=3D&ZZg z7i>7m{nCMAYe4-=rva<6Gf_nf`Qvb4rRUHjMl#I&Adq;6Q3^K{PSgE%$6Y98q?^C1! z3BjcsmMI~uNi`8H{*^ko9oDw-oDl_B1A%a zKGJ}7(MUKHk=qWb=Jxuc&@Cw3uamk|T_-od+ua(Ve`R4HqPgNyJOSEzs>Mx^@6d({ z%R|TM2yD4((V)ZE!uT*C)i#a>ih2%2bx6LyNRN&dNQX!oT%b>o5hPg&vks2Ly>u3w zA<-M?EZfmt9T!oD!7nHYb(wlvO?4VvRlnFVV%ob6Zl(Hdcw4J4t+}nTo%Q*UQV$7shD%*7{jZB_{3h1s249 z?6@2;06T6Mg{5DME+q&2EIV)|8D)RyQ{6jqN^kC+%|6 zO*_@K+cmrRI(o4YCvC%>!m&HT+mF0$7`DZ>cz1Q{1gD0&aiW;_}4zpGzp3p!r}ZV|8zqp0`+d?+663qzoV1R0)}pX3`qGe0R1g@JwyW}`hENbNMxf7v_8N;OR62>y=K1r%SRV(Qw0>^# ztU0ef6Lr!|Qb@U4W)hg9YS3^Ajd>eGRr$fJ|`t@?A&CGh6JC3GT%Uu{zhuEe9M z-$}-Z{>~hzQfbc|eeJLRN&_zlNvGYL{^kAxBu(1IzJ0GB!ZEmicX6LZ_tjg-ZH+8L z;MXjKVK^+4A-y@fFG>Ej?M*fjt=%Ri=3)71SCx2Lff#s#GpzanQwC*YH zC;}j;K1x@}FMFjJa#bQDhI&{8A{j+Uz;PIy-~f(nzuv$1k~47_YShZ~yeLBjjwIc< zg-HjyqqbJ|hd{J&lFdgI`$(19!Nqcfib*c=p+K`01|00BsnB#4A=BmFLdYLk31PV= z7B^zUU)?}7=95#4RBE$SWk5UP@R!zM??gzTBh<7oFTtUM0{zvR9J#g|1ewQBXFwZi zf?5+83B%tdhsUuo)Orn)V`Her43YYxdvQIeFE}#+c+4J{aiHr&NIi}FVu;)q-P>K` zzWB+xFWR7~8uvvq>#~}-TjRcPIviqy+iDfCf}qgoun0ki%i?gClYs)JG*J}El|ph> zFguVl*O%iIqFGjJeU{5f@fN`;Iz>m*SZj-rV+rP$T7(L>5H58)iz^!KLWNjbRFrFL z#GMPuiz2>ei``qB-z=9~FlUj{Xf0Cji?+rb``G+yLAEzy&G~Mj+37B6c8WzrzE~`! zuHjC1Me16<EGwnP`m#zBJ|YY-d%7C(i8q$H1-B*WtFO|yFEs9p z+ES|f_^>s?=qtuF?u$~5`$FTs(6}!&?hB3kLgT*BxGyyBi_Qu8H0}#e*3T&=-%rnd mL6N|6LCBKP&k%kiMfwS&*N+oM8K?a+8}NELiSCE{;{O5j&#veI literal 0 HcmV?d00001 diff --git a/issues/53036/issue_data.json b/issues/53036/issue_data.json new file mode 100644 index 0000000..e15738d --- /dev/null +++ b/issues/53036/issue_data.json @@ -0,0 +1,5 @@ +{ + "price": 250.0, + "title": "[HOLD for payment 2024-12-16] [$250] No way to save a search after searching for a custom query", + "issue_repo_steps": "Action Performed:\nGo to search\nClick on search\nEnter any query to give results\nClick filters\nExpected Result: Should have an option to save the results\nActual Result: No option to save the search for a custom query" +} \ No newline at end of file diff --git a/issues/53036/issue_introduction.patch b/issues/53036/issue_introduction.patch new file mode 100644 index 0000000..e69de29 diff --git a/issues/53036/revert_command.txt b/issues/53036/revert_command.txt new file mode 100644 index 0000000..6fb2a02 --- /dev/null +++ b/issues/53036/revert_command.txt @@ -0,0 +1 @@ +git reset --hard cefad67164cf7a40f65b50bba2126df09e9b31bb \ No newline at end of file diff --git a/issues/53036/test.py b/issues/53036/test.py new file mode 100644 index 0000000..633e533 --- /dev/null +++ b/issues/53036/test.py @@ -0,0 +1,128 @@ + +""" +Verify that Save Search option is present on applying custom filters + +""" +import pytest +import os +from playwright.async_api import Page, expect, async_playwright +from utils.login import NEW_DOT_URL, sign_in +from utils.browser import close_browser, start_browser + +STATIC_PREFIX = 53036 +EXPENSIFY_USERNAME_FIRSTNAME = "osaidtest" +EXPENSIFY_USERNAME_LASTNAME = "testaccount" +EXPENSIFY_USERNAME = f"{EXPENSIFY_USERNAME_FIRSTNAME} {EXPENSIFY_USERNAME_LASTNAME}" +EXPENSIFY_EMAIL_USERNAME = "osaidtest5" +EXPENSIFY_EMAIL = f"osaidtest5+{STATIC_PREFIX}@gmail.com" +EXPENSIFY_PASSWORD = "ntvd acej mmaa qqmd" +DATA_DIR = "browser_context" +WORKSPACE_NAME = "workspace1" +SLOW_MO = 1000 +NORMAL_RUN = False +PROXY_SETTINGS = {"server": "http://localhost:8080"} +SEARCH_QUERY = "type:expense status:all from:osaidtest5+530366@gmail.com to:osaidtest5+53036@gmail.com" +SECOND_USER_USERNAME = 'second usr' + +async def sign_in_recorded(page: Page, email: str): + await page.get_by_test_id("username").fill(email) + await page.get_by_role("button", name="Continue").click() + await page.get_by_test_id("validateCode").fill("123456") + +async def start_browser( + headless=False, + persistent=False, + data_dir=None, + slow_mo=500, + launch_args=["--ignore-certificate-errors"], + proxy=None, +): + """ + Start a browser instance with the given parameters. + + :param headless: Boolean to specify if the browser should run in headless mode. + :param persistent: Boolean to specify if the browser context should be persistent. + :param data_dir: Directory to store browser data for persistent context. + :return: A tuple of (context, page, playwright). + """ + + # Initialize Playwright + playwright = await async_playwright().start() + device = playwright.devices["Desktop Safari"] + context, page = None, None + if persistent: + if data_dir is None: + data_dir = "browser_context" + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + context = await playwright.chromium.launch_persistent_context( + data_dir, + proxy=proxy, + headless=headless, + args=launch_args, + slow_mo=slow_mo, + timezone_id="Asia/Karachi", + **device + ) + page = context.pages[0] + else: + browser = await playwright.chromium.launch( + headless=headless, args=launch_args, slow_mo=slow_mo, proxy=proxy, + ) + context = await browser.new_context( + ignore_https_errors=True, timezone_id="Asia/Karachi", permissions=["clipboard-read", "clipboard-write"], **device + ) + page = await context.new_page() + + return context, page, playwright # Return playwright to close later + + +@pytest.mark.asyncio +async def test_53036(): + try: + context, page, playwright = await start_browser( + persistent=False, + data_dir=DATA_DIR, + headless=False, + slow_mo=SLOW_MO, + launch_args=[ + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process", + ], + proxy=PROXY_SETTINGS + ) + + # Step 1: Sign in + if NORMAL_RUN: + # Normal test run + await sign_in(page, EXPENSIFY_EMAIL, password=EXPENSIFY_PASSWORD) + else: + # Recorded Test Run. + await page.goto(NEW_DOT_URL) + await sign_in_recorded(page=page, email=EXPENSIFY_EMAIL) + + await page.get_by_test_id("CustomBottomTabNavigator").get_by_label("Search").click() + + # Cleanup (Reset Filters if present) + await page.get_by_role("button", name="Filters").click() + if await page.get_by_role("link", name="Reset filters").is_visible(): + await page.get_by_role("link", name="Reset filters").click() + await page.get_by_label("Back").click() + + await page.get_by_test_id("Search").get_by_label("Search").click() + await page.get_by_test_id("search-router-text-input").fill(SEARCH_QUERY) + await page.get_by_test_id("search-router-text-input").press("Enter") + + await page.get_by_role("button", name="Filters").click() + + # Assert that the filters are correctly applied. + await expect(page.get_by_test_id("SearchAdvancedFiltersPage").get_by_text(SECOND_USER_USERNAME)).to_be_visible() + await expect(page.get_by_test_id("SearchAdvancedFiltersPage").get_by_text(EXPENSIFY_USERNAME)).to_be_visible() + await expect(page.get_by_role("button", name="Save search")).to_be_visible() + + + finally: + # Step 6: Close the browser. + await close_browser(context, page, playwright) \ No newline at end of file diff --git a/issues/53036/user_tool.py b/issues/53036/user_tool.py new file mode 100644 index 0000000..1c1a327 --- /dev/null +++ b/issues/53036/user_tool.py @@ -0,0 +1,133 @@ +" +Verify that Save Search option is present on applying custom filters +"" +import pytest +import os +from playwright.async_api import Page, expect, async_playwright +from utils.login import NEW_DOT_URL, sign_in +from utils.browser import close_browser, start_browser +from utils.trace_cleaner import trace_cleaner + +STATIC_PREFIX = 53036 +EXPENSIFY_USERNAME_FIRSTNAME = "osaidtest" +EXPENSIFY_USERNAME_LASTNAME = "testaccount" +EXPENSIFY_USERNAME = f"{EXPENSIFY_USERNAME_FIRSTNAME} {EXPENSIFY_USERNAME_LASTNAME}" +EXPENSIFY_EMAIL_USERNAME = "osaidtest5" +EXPENSIFY_EMAIL = f"osaidtest5+{STATIC_PREFIX}@gmail.com" +EXPENSIFY_PASSWORD = "ntvd acej mmaa qqmd" +DATA_DIR = "browser_context" +WORKSPACE_NAME = "workspace1" +SLOW_MO = 1000 +NORMAL_RUN = False +PROXY_SETTINGS = {"server": "http://localhost:8080"} +SEARCH_QUERY = "type:expense status:all from:osaidtest5+530366@gmail.com to:osaidtest5+53036@gmail.com" +SECOND_USER_USERNAME = 'second usr' + + +async def sign_in_recorded(page: Page, email: str): + await page.get_by_test_id("username").fill(email) + await page.get_by_role("button", name="Continue").click() + await page.get_by_test_id("validateCode").fill("123456") + + +async def start_browser( + headless=False, + persistent=False, + data_dir=None, + slow_mo=500, + launch_args=["--ignore-certificate-errors"], + proxy=None, +): + """ + Start a browser instance with the given parameters. + + :param headless: Boolean to specify if the browser should run in headless mode. + :param persistent: Boolean to specify if the browser context should be persistent. + :param data_dir: Directory to store browser data for persistent context. + :return: A tuple of (context, page, playwright). + """ + + # Initialize Playwright + playwright = await async_playwright().start() + device = playwright.devices["Desktop Safari"] + context, page = None, None + if persistent: + if data_dir is None: + data_dir = "browser_context" + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + context = await playwright.chromium.launch_persistent_context( + data_dir, + proxy=proxy, + headless=headless, + args=launch_args, + slow_mo=slow_mo, + timezone_id="Asia/Karachi", + **device + ) + page = context.pages[0] + else: + browser = await playwright.chromium.launch( + headless=headless, args=launch_args, slow_mo=slow_mo, proxy=proxy, + ) + context = await browser.new_context( + ignore_https_errors=True, timezone_id="Asia/Karachi", permissions=["clipboard-read", "clipboard-write"], **device + ) + page = await context.new_page() + + return context, page, playwright # Return playwright to close later + + +@pytest.mark.asyncio +async def test_53036(): + context, page, playwright = await start_browser( + persistent=False, + data_dir=DATA_DIR, + headless=False, + slow_mo=SLOW_MO, + launch_args=[ + "--ignore-certificate-errors", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process", + ], + proxy=PROXY_SETTINGS + ) + + # Start tracing + await context.tracing.start(screenshots=True, snapshots=True, sources=True) + + try: + # Step 1: Sign in + if NORMAL_RUN: + # Normal test run + await sign_in(page, EXPENSIFY_EMAIL, password=EXPENSIFY_PASSWORD) + else: + # Recorded Test Run. + await page.goto(NEW_DOT_URL) + await sign_in_recorded(page=page, email=EXPENSIFY_EMAIL) + + await page.get_by_test_id("CustomBottomTabNavigator").get_by_label("Search").click() + + # Cleanup (Reset Filters if present) + await page.get_by_role("button", name="Filters").click() + if await page.get_by_role("link", name="Reset filters").is_visible(): + await page.get_by_role("link", name="Reset filters").click() + await page.get_by_label("Back").click() + + await page.get_by_test_id("Search").get_by_label("Search").click() + await page.get_by_test_id("search-router-text-input").fill(SEARCH_QUERY) + await page.get_by_test_id("search-router-text-input").press("Enter") + + await page.get_by_role("button", name="Filters").click() + + # Assert that the filters are correctly applied. + await expect(page.get_by_test_id("SearchAdvancedFiltersPage").get_by_text(SECOND_USER_USERNAME)).to_be_visible() + await expect(page.get_by_test_id("SearchAdvancedFiltersPage").get_by_text(EXPENSIFY_USERNAME)).to_be_visible() + await expect(page.get_by_role("button", name="Save search")).to_be_visible() + + finally: + trace_path = "/app/expensify/user_tool/output_browser1.zip" + await context.tracing.stop(path=trace_path) + trace_cleaner(trace_path) + await close_browser(context, page, playwright)