diff --git a/scripts/grdm.py b/scripts/grdm.py index 0497acd..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: # プロジェクトが存在しない @@ -371,26 +372,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() 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() 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", + "" ] }, {