From 30f3fe02f355fed4c0efbf9d242fbb4cf9a1ae02 Mon Sep 17 00:00:00 2001 From: yacchin1205 <968739+yacchin1205@users.noreply.github.com> Date: Wed, 19 Aug 2026 07:37:33 +0900 Subject: [PATCH 1/4] Fix post-login screenshot race and unscoped disable-button locators in workflow addon E2E --- ...343\201\256\350\250\255\345\256\232.ipynb" | 9 ++- ...347\204\241\345\212\271\345\214\226.ipynb" | 56 ++++++++++++------- ...346\234\211\345\212\271\345\214\226.ipynb" | 9 ++- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\345\205\254\351\226\213\347\257\204\345\233\262\343\201\256\350\250\255\345\256\232.ipynb" "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\345\205\254\351\226\213\347\257\204\345\233\262\343\201\256\350\250\255\345\256\232.ipynb" index e5610cc..dac3bc6 100644 --- "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\345\205\254\351\226\213\347\257\204\345\233\262\343\201\256\350\250\255\345\256\232.ipynb" +++ "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\345\205\254\351\226\213\347\257\204\345\233\262\343\201\256\350\250\255\345\256\232.ipynb" @@ -621,7 +621,10 @@ "id": "837d6c63", "metadata": {}, "source": [ - "## テンプレート管理プロジェクトのURLを開き、テンプレート登録者でログインする" + "## テンプレート管理プロジェクトのURLを開き、テンプレート登録者でログインする\n", + "\n", + "プロジェクトページが表示されること\n", + "" ] }, { @@ -634,8 +637,10 @@ "async def _step(page):\n", " await page.goto(template_project_url)\n", " await grdm.login(page, idp_name_registrar, idp_username_registrar, idp_password_registrar, transition_timeout=transition_timeout)\n", + " await expect(page.locator('//span[@id = \"nodeTitleEditable\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { diff --git "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\346\234\211\345\212\271\345\214\226\343\203\273\347\204\241\345\212\271\345\214\226.ipynb" "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\346\234\211\345\212\271\345\214\226\343\203\273\347\204\241\345\212\271\345\214\226.ipynb" index 48363b9..2046280 100644 --- "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\346\234\211\345\212\271\345\214\226\343\203\273\347\204\241\345\212\271\345\214\226.ipynb" +++ "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\346\234\211\345\212\271\345\214\226\343\203\273\347\204\241\345\212\271\345\214\226.ipynb" @@ -205,10 +205,12 @@ "async def _step(page):\n", " await page.locator('//a[text() = \"アドオン\"]').click()\n", " await expect(page.locator('//h3[text() = \"アドオンを選択\"]')).to_be_visible(timeout=transition_timeout)\n", - " await page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", - " await expect(page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", + " row = page.locator('#activationsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", + " await expect(row.locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -229,10 +231,12 @@ "outputs": [], "source": [ "async def _step(page):\n", - " await page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]').click()\n", + " row = page.locator('#activationsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').click()\n", " await expect(page.locator('//h4[text() = \"ワークフローの無効化\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -455,10 +459,12 @@ "async def _step(page):\n", " await page.locator('//a[text() = \"アドオン\"]').click()\n", " await expect(page.locator('//h3[text() = \"アドオンを選択\"]')).to_be_visible(timeout=transition_timeout)\n", - " await page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", - " await expect(page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", + " row = page.locator('#activationsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", + " await expect(row.locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -478,12 +484,14 @@ "outputs": [], "source": [ "async def _step(page):\n", - " await page.locator('#activationsPanel').locator('//button[*[text() = \"無効にする\"]]').click()\n", + " row = page.locator('#activationsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').click()\n", " await expect(page.locator('//h4[text() = \"ワークフローの無効化\"]')).to_be_visible(timeout=transition_timeout)\n", " await page.locator('//button[contains(@data-bind, \"confirmDisableActivation\")]').click()\n", - " await expect(page.locator('#activationsPanel').locator('//*[contains(@class, \"label\") and text() = \"無効\"]')).to_be_visible(timeout=transition_timeout)\n", + " await expect(row.locator('//*[contains(@class, \"label\") and text() = \"無効\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -775,10 +783,12 @@ "async def _step(page):\n", " await page.locator('//a[text() = \"アドオン\"]').click()\n", " await expect(page.locator('//h3[text() = \"アドオンを選択\"]')).to_be_visible(timeout=transition_timeout)\n", - " await page.locator('#localWorkflowsPanel').locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", - " await expect(page.locator('#localWorkflowsPanel').locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", + " row = page.locator('#localWorkflowsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", + " await expect(row.locator('//button[*[text() = \"無効にする\"]]')).to_be_enabled(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -799,10 +809,12 @@ "outputs": [], "source": [ "async def _step(page):\n", - " await page.locator('#localWorkflowsPanel').locator('//button[*[text() = \"無効にする\"]]').click()\n", + " row = page.locator('#localWorkflowsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').click()\n", " await expect(page.locator('//h4[text() = \"ワークフローテンプレートの無効化\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -916,13 +928,15 @@ "outputs": [], "source": [ "async def _step(page):\n", - " await page.locator('#localWorkflowsPanel').locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", - " await page.locator('#localWorkflowsPanel').locator('//button[*[text() = \"無効にする\"]]').click()\n", + " row = page.locator('#localWorkflowsPanel').locator(f'//tr[.//strong[text()=\"{workflow_name}\"]]')\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').scroll_into_view_if_needed()\n", + " await row.locator('//button[*[text() = \"無効にする\"]]').click()\n", " await expect(page.locator('//h4[text() = \"ワークフローテンプレートの無効化\"]')).to_be_visible(timeout=transition_timeout)\n", " await page.locator('//button[contains(@data-bind, \"confirmDisableTemplate\")]').click()\n", - " await expect(page.locator('#localWorkflowsPanel').locator('//*[contains(@class, \"label\") and text() = \"無効\"]')).to_be_visible(timeout=transition_timeout)\n", + " await expect(row.locator('//*[contains(@class, \"label\") and text() = \"無効\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { @@ -1635,4 +1649,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\350\207\252\345\213\225\346\234\211\345\212\271\345\214\226.ipynb" "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\350\207\252\345\213\225\346\234\211\345\212\271\345\214\226.ipynb" index fcbc09a..b20c593 100644 --- "a/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\350\207\252\345\213\225\346\234\211\345\212\271\345\214\226.ipynb" +++ "b/\343\203\206\343\202\271\343\203\210\346\211\213\351\240\206-Workflow\343\202\242\343\203\211\343\202\252\343\203\263-\350\207\252\345\213\225\346\234\211\345\212\271\345\214\226.ipynb" @@ -654,7 +654,10 @@ "id": "2816ee79", "metadata": {}, "source": [ - "## テンプレート管理プロジェクトのURLを開き、テンプレート登録者でログインする" + "## テンプレート管理プロジェクトのURLを開き、テンプレート登録者でログインする\n", + "\n", + "プロジェクトページが表示されること\n", + "" ] }, { @@ -667,8 +670,10 @@ "async def _step(page):\n", " await page.goto(template_project_url)\n", " await grdm.login(page, idp_name_registrar, idp_username_registrar, idp_password_registrar, transition_timeout=transition_timeout)\n", + " await expect(page.locator('//span[@id = \"nodeTitleEditable\"]')).to_be_visible(timeout=transition_timeout)\n", "\n", - "await run_pw(_step)" + "await run_pw(_step)\n", + "" ] }, { From 6f4a49bf7fdded9cc76460e1aef052648db498b5 Mon Sep 17 00:00:00 2001 From: yacchin1205 <968739+yacchin1205@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:36:07 +0900 Subject: [PATCH 2/4] Raise default expect timeout to match transition_timeout convention --- scripts/playwright.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/playwright.py b/scripts/playwright.py index 2608849..9334f26 100644 --- a/scripts/playwright.py +++ b/scripts/playwright.py @@ -134,6 +134,9 @@ async def init_pw_context(close_on_fail=True, last_path=None, browser_type='chro await playwright.stop() playwright = None playwright = await async_playwright().start() + # timeout未指定のexpectはライブラリ既定の5秒に落ちてタイミング起因の失敗源になる + # ため、既定を transition_timeout の慣行値へ引き上げる (明示指定は影響を受けない) + expect.set_options(timeout=60000) current_session_id = datetime.now().strftime('%Y%m%d-%H%M%S') default_last_path = last_path or os.path.join(os.path.expanduser('~/last-screenshots'), current_session_id) temp_dir = tempfile.mkdtemp() From af0afbc57bc0c0f263d3bbf1ec4c044da05830a0 Mon Sep 17 00:00:00 2001 From: yacchin1205 <968739+yacchin1205@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:29:44 +0900 Subject: [PATCH 3/4] Scroll targets into view and wait for layout to settle before drag and drop --- scripts/grdm.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/scripts/grdm.py b/scripts/grdm.py index 0497acd..8136ed0 100644 --- a/scripts/grdm.py +++ b/scripts/grdm.py @@ -371,26 +371,39 @@ async def drop_file(page, element_locator, path): async def drag_and_drop(page, source, dest): await expect(source).to_have_class(re.compile('.*ui-draggable.*')) - center_coordinates_source = await source.evaluate('''element => { - const rect = element.getBoundingClientRect(); - return { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - }; - }''') + # ドロップ先→ソースの順で可視化し、両方がビューポート内にあることを保証する + await dest.scroll_into_view_if_needed() + await source.scroll_into_view_if_needed() + await expect(dest).to_be_in_viewport() + await expect(source).to_be_in_viewport() + + async def centers(): + result = [] + for locator in (source, dest): + result.append(await locator.evaluate('''element => { + const rect = element.getBoundingClientRect(); + return [rect.left + rect.width / 2, rect.top + rect.height / 2]; + }''')) + return result + + # jQuery UIはドラッグ開始時にドロップ先の位置をキャッシュするため、開始前に + # レイアウトが静止していること(バナー出現や再描画の途中でないこと)を確認する + previous = await centers() + for _ in range(25): + await page.wait_for_timeout(200) + current = await centers() + if current == previous: + break + previous = current + else: + raise AssertionError(f'layout did not settle before drag and drop: {current}') - center_coordinates_dest = await dest.evaluate('''element => { - const rect = element.getBoundingClientRect(); - return { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - }; - }''') + (source_x, source_y), (dest_x, dest_y) = current - await page.mouse.move(center_coordinates_source['x'], center_coordinates_source['y']) + await page.mouse.move(source_x, source_y) await page.mouse.down() await page.wait_for_timeout(1000) - await page.mouse.move(center_coordinates_dest['x'], center_coordinates_dest['y'], steps=30) + await page.mouse.move(dest_x, dest_y, steps=30) await page.wait_for_timeout(1000) await page.mouse.up() From 6e2c8bb1114c5b7445e2c3090a874ebe92d4e5c8 Mon Sep 17 00:00:00 2001 From: yacchin1205 <968739+yacchin1205@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:44:45 +0900 Subject: [PATCH 4/4] Give the project existence probe its own short timeout parameter --- scripts/grdm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/grdm.py b/scripts/grdm.py index 8136ed0..735a636 100644 --- a/scripts/grdm.py +++ b/scripts/grdm.py @@ -210,10 +210,11 @@ async def expect_dashboard(page, transition_timeout=30000, retries=3): # 1分待って再チャレンジ await asyncio.sleep(60) -async def ensure_project_exists(page, project_name, transition_timeout=30000): +async def ensure_project_exists(page, project_name, transition_timeout=30000, existence_timeout=10000): await expect(page.locator('//*[@data-test-create-project-modal-button]')).to_have_count(1, timeout=transition_timeout) try: - await expect(page.locator(f'//*[@data-test-dashboard-item-title and text()="{project_name}"]')).to_be_visible() + # 不存在(タイムアウト)が作成フローへの正常分岐のため、既定の長いexpectタイムアウトを使わない + await expect(page.locator(f'//*[@data-test-dashboard-item-title and text()="{project_name}"]')).to_be_visible(timeout=existence_timeout) return False except: # プロジェクトが存在しない