From e61316fe959a2bf2cb9b4a17bffc87abe76bc1d0 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Wed, 1 Oct 2025 17:34:28 -0300 Subject: [PATCH] fix: Correct read_project pagination logic and improve test stability (nightly fix) (#10076) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/api/v1/projects.py | 17 +- .../tests/unit/api/v1/test_projects.py | 213 +++++++++++++++++- src/frontend/playwright.config.ts | 3 +- .../starter-projects-shard1.spec.ts | 132 +++++++++-- .../starter-projects-shard2.spec.ts | 132 +++++++++-- .../starter-projects-shard3.spec.ts | 132 +++++++++-- .../starter-projects-shard4.spec.ts | 132 +++++++++-- 7 files changed, 661 insertions(+), 100 deletions(-) diff --git a/src/backend/base/langflow/api/v1/projects.py b/src/backend/base/langflow/api/v1/projects.py index 35c848cd8c..f7da36ad3d 100644 --- a/src/backend/base/langflow/api/v1/projects.py +++ b/src/backend/base/langflow/api/v1/projects.py @@ -7,7 +7,7 @@ from urllib.parse import quote from uuid import UUID import orjson -from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Response, UploadFile, status +from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Query, Response, UploadFile, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from fastapi_pagination import Params @@ -228,6 +228,8 @@ async def read_project( project_id: UUID, current_user: CurrentActiveUser, params: Annotated[Params | None, Depends(custom_params)], + page: Annotated[int | None, Query()] = None, + size: Annotated[int | None, Query()] = None, is_component: bool = False, is_flow: bool = False, search: str = "", @@ -249,7 +251,8 @@ async def read_project( raise HTTPException(status_code=404, detail="Project not found") try: - if params and params.page and params.size: + # Check if pagination is explicitly requested by the user (both page and size provided) + if page is not None and size is not None: stmt = select(Flow).where(Flow.folder_id == project_id) if Flow.updated_at is not None: @@ -260,6 +263,7 @@ async def read_project( stmt = stmt.where(Flow.is_component == False) # noqa: E712 if search: stmt = stmt.where(Flow.name.like(f"%{search}%")) # type: ignore[attr-defined] + import warnings with warnings.catch_warnings(): @@ -270,13 +274,14 @@ async def read_project( return FolderWithPaginatedFlows(folder=FolderRead.model_validate(project), flows=paginated_flows) + # If no pagination requested, return all flows for the current user + flows_from_current_user_in_project = [flow for flow in project.flows if flow.user_id == current_user.id] + project.flows = flows_from_current_user_in_project + return project # noqa: TRY300 + except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e - flows_from_current_user_in_project = [flow for flow in project.flows if flow.user_id == current_user.id] - project.flows = flows_from_current_user_in_project - return project - @router.patch("/{project_id}", response_model=FolderRead, status_code=200) async def update_project( diff --git a/src/backend/tests/unit/api/v1/test_projects.py b/src/backend/tests/unit/api/v1/test_projects.py index 87c79eafcf..e890a46b27 100644 --- a/src/backend/tests/unit/api/v1/test_projects.py +++ b/src/backend/tests/unit/api/v1/test_projects.py @@ -476,7 +476,7 @@ class TestProjectMCPIntegration: # Mock API key creation mock_api_key_response = MagicMock() - mock_api_key_response.api_key = "test-api-key-123" + mock_api_key_response.api_key = "test-api-key-123" # pragma: allowlist secret mock_create_api_key.return_value = mock_api_key_response # Mock validation - no conflict @@ -885,3 +885,214 @@ class TestProjectMCPIntegration: result = response.json() assert "name" in result assert result["name"] == basic_case["name"] + + +# Tests for the read_project bug fix +class TestReadProjectBugFix: + """Test the read_project endpoint fix for ASGI response bug.""" + + async def test_read_project_without_pagination_params(self, client: AsyncClient, logged_in_headers, basic_case): + """Test read_project returns correct response when no pagination params are provided.""" + # Create a project first + create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers) + assert create_response.status_code == status.HTTP_201_CREATED + project_id = create_response.json()["id"] + + # Read project without pagination params + response = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers) + + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return FolderReadWithFlows (direct project response) + assert isinstance(result, dict) + assert "name" in result + assert "description" in result + assert "id" in result + assert "flows" in result + assert result["name"] == basic_case["name"] + + async def test_read_project_with_pagination_params(self, client: AsyncClient, logged_in_headers, basic_case): + """Test read_project returns paginated response when pagination params are provided.""" + # Create a project first + create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers) + assert create_response.status_code == status.HTTP_201_CREATED + project_id = create_response.json()["id"] + + # Read project with pagination params + response = await client.get(f"api/v1/projects/{project_id}?page=1&size=10", headers=logged_in_headers) + + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return FolderWithPaginatedFlows (paginated response) + assert isinstance(result, dict) + assert "folder" in result + assert "flows" in result + + # Check folder structure + folder = result["folder"] + assert "name" in folder + assert "description" in folder + assert "id" in folder + assert folder["name"] == basic_case["name"] + + # Check flows pagination structure + flows = result["flows"] + assert "items" in flows + assert "total" in flows + assert "page" in flows + assert "size" in flows + + async def test_read_project_with_partial_pagination_params( + self, client: AsyncClient, logged_in_headers, basic_case + ): + """Test read_project behavior when only some pagination params are provided.""" + # Create a project first + create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers) + assert create_response.status_code == status.HTTP_201_CREATED + project_id = create_response.json()["id"] + + # Test with only page param (no size) + response = await client.get(f"api/v1/projects/{project_id}?page=1", headers=logged_in_headers) + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return non-paginated response (FolderReadWithFlows) + assert isinstance(result, dict) + assert "name" in result # Direct project response + assert "flows" in result + assert result["name"] == basic_case["name"] + + # Test with only size param (no page) + response = await client.get(f"api/v1/projects/{project_id}?size=10", headers=logged_in_headers) + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return non-paginated response (FolderReadWithFlows) + assert isinstance(result, dict) + assert "name" in result # Direct project response + assert "flows" in result + assert result["name"] == basic_case["name"] + + async def test_read_project_with_filtering_params(self, client: AsyncClient, logged_in_headers, basic_case): + """Test read_project with filtering parameters (is_component, is_flow, search).""" + # Create a project first + create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers) + assert create_response.status_code == status.HTTP_201_CREATED + project_id = create_response.json()["id"] + + # Create a flow and component in the project for filtering tests + flow_payload = { + "name": "Test Flow", + "description": "A test flow", + "folder_id": project_id, + "data": {"nodes": [], "edges": []}, + "is_component": False, + } + component_payload = { + "name": "Test Component", + "description": "A test component", + "folder_id": project_id, + "data": {"nodes": [], "edges": []}, + "is_component": True, + } + + flow_response = await client.post("api/v1/flows/", json=flow_payload, headers=logged_in_headers) + comp_response = await client.post("api/v1/flows/", json=component_payload, headers=logged_in_headers) + assert flow_response.status_code == status.HTTP_201_CREATED + assert comp_response.status_code == status.HTTP_201_CREATED + + # Test with filtering params but no pagination (should use non-paginated path) + response = await client.get(f"api/v1/projects/{project_id}?is_flow=true", headers=logged_in_headers) + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return non-paginated response + assert isinstance(result, dict) + assert "name" in result + assert "flows" in result + + # Test with filtering params AND pagination (should use paginated path) + response = await client.get( + f"api/v1/projects/{project_id}?is_flow=true&page=1&size=10", headers=logged_in_headers + ) + assert response.status_code == status.HTTP_200_OK + result = response.json() + + # Should return paginated response + assert isinstance(result, dict) + assert "folder" in result + assert "flows" in result + assert "items" in result["flows"] + + async def test_read_project_consistent_response_structure(self, client: AsyncClient, logged_in_headers, basic_case): + """Test that read_project returns consistent response structure in all cases.""" + # Create a project first + create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers) + assert create_response.status_code == status.HTTP_201_CREATED + project_id = create_response.json()["id"] + + # Test multiple request scenarios to ensure consistency + test_cases = [ + # No params - should return FolderReadWithFlows + {"params": "", "expect_paginated": False}, + # Only search - should return FolderReadWithFlows + {"params": "?search=test", "expect_paginated": False}, + # Only is_component - should return FolderReadWithFlows + {"params": "?is_component=true", "expect_paginated": False}, + # Only is_flow - should return FolderReadWithFlows + {"params": "?is_flow=true", "expect_paginated": False}, + # Only page - should return FolderReadWithFlows + {"params": "?page=1", "expect_paginated": False}, + # Only size - should return FolderReadWithFlows + {"params": "?size=10", "expect_paginated": False}, + # Both page and size - should return FolderWithPaginatedFlows + {"params": "?page=1&size=10", "expect_paginated": True}, + # Page, size and filters - should return FolderWithPaginatedFlows + {"params": "?page=1&size=10&is_flow=true", "expect_paginated": True}, + ] + + for test_case in test_cases: + response = await client.get(f"api/v1/projects/{project_id}{test_case['params']}", headers=logged_in_headers) + assert response.status_code == status.HTTP_200_OK, f"Failed for params: {test_case['params']}" + + result = response.json() + assert isinstance(result, dict), f"Result should be dict for params: {test_case['params']}" + + if test_case["expect_paginated"]: + # Paginated response structure + assert "folder" in result, f"Paginated response missing 'folder' for params: {test_case['params']}" + assert "flows" in result, f"Paginated response missing 'flows' for params: {test_case['params']}" + assert "items" in result["flows"], f"Paginated flows missing 'items' for params: {test_case['params']}" + assert "total" in result["flows"], f"Paginated flows missing 'total' for params: {test_case['params']}" + else: + # Non-paginated response structure + assert "name" in result, f"Non-paginated response missing 'name' for params: {test_case['params']}" + assert "flows" in result, f"Non-paginated response missing 'flows' for params: {test_case['params']}" + # Should NOT have pagination structure + assert "folder" not in result, ( + f"Non-paginated response should not have 'folder' for params: {test_case['params']}" + ) + + async def test_read_project_error_handling_consistency(self, client: AsyncClient, logged_in_headers): + """Test that error handling is consistent across both response paths.""" + import uuid + + non_existent_id = str(uuid.uuid4()) + + # Test both pagination and non-pagination paths with non-existent project + test_cases = [ + "", # Non-paginated path + "?page=1&size=10", # Paginated path + ] + + for params in test_cases: + response = await client.get(f"api/v1/projects/{non_existent_id}{params}", headers=logged_in_headers) + assert response.status_code == status.HTTP_404_NOT_FOUND, f"Should return 404 for params: {params}" + + result = response.json() + assert "detail" in result, f"Error response should have 'detail' for params: {params}" + assert "not found" in result["detail"].lower(), ( + f"Error message should mention 'not found' for params: {params}" + ) diff --git a/src/frontend/playwright.config.ts b/src/frontend/playwright.config.ts index 0ffd8d7774..4c2a313c87 100644 --- a/src/frontend/playwright.config.ts +++ b/src/frontend/playwright.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: 2, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - timeout: 3 * 60 * 750, + timeout: 5 * 60 * 1000, // 5 minutes // reporter: [ // ["html", { open: "never", outputFolder: "playwright-report/test-results" }], // ], @@ -121,6 +121,7 @@ export default defineConfig({ env: { VITE_PROXY_TARGET: "http://localhost:7860", }, + reuseExistingServer: true, }, ], }); diff --git a/src/frontend/tests/core/integrations/starter-projects-shard1.spec.ts b/src/frontend/tests/core/integrations/starter-projects-shard1.spec.ts index b3de9cad17..b3f60d33c3 100644 --- a/src/frontend/tests/core/integrations/starter-projects-shard1.spec.ts +++ b/src/frontend/tests/core/integrations/starter-projects-shard1.spec.ts @@ -1,6 +1,35 @@ import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; +// Helper function to ensure element stability before interaction +async function waitForElementStability( + page: any, + selector: string, + timeout = 5000, +) { + await page.waitForSelector(selector, { state: "visible", timeout }); + // Additional wait to ensure element is stable and not moving + await page.waitForTimeout(200); + // Use first() to avoid strict mode violation when multiple elements exist + await expect(page.locator(selector).first()).toBeVisible(); +} + +// Helper function to safely navigate with proper waiting +async function safeNavigateAndClick( + page: any, + testId: string, + waitForSelector?: string, +) { + const element = page.getByTestId(testId); + await expect(element).toBeVisible({ timeout: 15000 }); + await element.click(); + + if (waitForSelector) { + await page.waitForLoadState("domcontentloaded"); + await waitForElementStability(page, waitForSelector); + } +} + test( "user should be able to use third quarter of starter projects without any outdated components on the flow", { tag: ["@release", "@components"] }, @@ -10,12 +39,33 @@ test( const templatesData = []; let numberOfOutdatedComponents = 0; - // First, collect all template data - await page.getByTestId("side_nav_options_all-templates").click(); + // First, collect all template data with proper waiting + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); - const numberOfTemplates = await page + // Wait for templates to fully load and stabilize + await page.waitForLoadState("networkidle", { timeout: 30000 }); + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); + + // Ensure template count is stable by checking multiple times + let numberOfTemplates = await page .getByTestId("text_card_container") .count(); + await page.waitForTimeout(1000); // Allow any dynamic loading to complete + const verifyCount = await page.getByTestId("text_card_container").count(); + + // If counts don't match, wait a bit more and try again + if (numberOfTemplates !== verifyCount) { + await page.waitForTimeout(2000); + numberOfTemplates = await page.getByTestId("text_card_container").count(); + } const secondQuarterEnd = Math.ceil((numberOfTemplates * 2) / 4); const thirdQuarterEnd = Math.ceil((numberOfTemplates * 3) / 4); @@ -24,13 +74,13 @@ test( `Total templates: ${numberOfTemplates}, Testing from ${secondQuarterEnd} to ${thirdQuarterEnd - 1} (third quarter)`, ); - // Collect template names first + // Collect template names first with stability checks for (let i = secondQuarterEnd; i < thirdQuarterEnd; i++) { - const exampleName = await page - .getByTestId("text_card_container") - .nth(i) - .getAttribute("role"); + // Ensure the specific template card is visible and stable + const templateCard = page.getByTestId("text_card_container").nth(i); + await expect(templateCard).toBeVisible({ timeout: 10000 }); + const exampleName = await templateCard.getAttribute("role"); templatesData.push({ index: i, name: exampleName }); } @@ -43,29 +93,62 @@ test( for (const template of templatesData) { console.log(`Testing template ${template.index}: ${template.name}`); - // Navigate directly to templates page - await page.goto("/"); + // Navigate directly to templates page with improved stability + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + + // Wait for main page to be fully loaded and stable await expect(page.getByTestId("mainpage_title")).toBeVisible({ timeout: 30000, }); - await page.getByTestId("new-project-btn").first().click(); - await page.getByTestId("side_nav_options_all-templates").click(); + await page.waitForLoadState("networkidle"); - // Wait for templates to load - await page.waitForSelector('[data-testid="text_card_container"]', { - timeout: 10000, - }); + // Sequential navigation with proper waiting (no Promise.all) + await safeNavigateAndClick(page, "new-project-btn"); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(500); // Brief pause between navigation steps - // Click on the specific template - await page.getByTestId("text_card_container").nth(template.index).click(); + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); + await page.waitForLoadState("networkidle"); - await page.waitForTimeout(1000); + // Ensure templates are fully loaded and stable before clicking + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); - await page.waitForSelector('[data-testid="div-generic-node"]', { - timeout: 15000, - }); + // Wait for the specific template to be visible and stable + const targetTemplate = page + .getByTestId("text_card_container") + .nth(template.index); + await expect(targetTemplate).toBeVisible({ timeout: 15000 }); + await page.waitForTimeout(300); // Ensure element is stable - if ((await page.getByTestId("update-all-button").count()) > 0) { + await targetTemplate.click(); + + // Wait for canvas to load properly with enhanced stability checks + await page.waitForLoadState("domcontentloaded"); + await page.waitForLoadState("networkidle", { timeout: 30000 }); + + // Wait for canvas elements to be visible and stable + await waitForElementStability( + page, + '[data-testid="div-generic-node"]', + 25000, + ); + + // Additional wait to ensure all canvas components are fully rendered + await page.waitForTimeout(1500); + + // Use auto-retrying assertion for checking outdated components + const updateButtonCount = await page + .getByTestId("update-all-button") + .count(); + if (updateButtonCount > 0) { console.error(` --------------------------------------------------------------------------------------- There's an outdated component on the basic template: ${template.name} @@ -73,6 +156,9 @@ test( `); numberOfOutdatedComponents++; } + + // Longer delay between template tests to prevent race conditions + await page.waitForTimeout(1000); } expect(numberOfOutdatedComponents).toBe(0); diff --git a/src/frontend/tests/core/integrations/starter-projects-shard2.spec.ts b/src/frontend/tests/core/integrations/starter-projects-shard2.spec.ts index abcb02aa46..80df102d05 100644 --- a/src/frontend/tests/core/integrations/starter-projects-shard2.spec.ts +++ b/src/frontend/tests/core/integrations/starter-projects-shard2.spec.ts @@ -1,6 +1,35 @@ import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; +// Helper function to ensure element stability before interaction +async function waitForElementStability( + page: any, + selector: string, + timeout = 5000, +) { + await page.waitForSelector(selector, { state: "visible", timeout }); + // Additional wait to ensure element is stable and not moving + await page.waitForTimeout(200); + // Use first() to avoid strict mode violation when multiple elements exist + await expect(page.locator(selector).first()).toBeVisible(); +} + +// Helper function to safely navigate with proper waiting +async function safeNavigateAndClick( + page: any, + testId: string, + waitForSelector?: string, +) { + const element = page.getByTestId(testId); + await expect(element).toBeVisible({ timeout: 15000 }); + await element.click(); + + if (waitForSelector) { + await page.waitForLoadState("domcontentloaded"); + await waitForElementStability(page, waitForSelector); + } +} + test( "user should be able to use first quarter of starter projects without any outdated components on the flow", { tag: ["@release", "@components"] }, @@ -10,12 +39,33 @@ test( const templatesData = []; let numberOfOutdatedComponents = 0; - // First, collect all template data - await page.getByTestId("side_nav_options_all-templates").click(); + // First, collect all template data with proper waiting + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); - const numberOfTemplates = await page + // Wait for templates to fully load and stabilize + await page.waitForLoadState("networkidle", { timeout: 30000 }); + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); + + // Ensure template count is stable by checking multiple times + let numberOfTemplates = await page .getByTestId("text_card_container") .count(); + await page.waitForTimeout(1000); // Allow any dynamic loading to complete + const verifyCount = await page.getByTestId("text_card_container").count(); + + // If counts don't match, wait a bit more and try again + if (numberOfTemplates !== verifyCount) { + await page.waitForTimeout(2000); + numberOfTemplates = await page.getByTestId("text_card_container").count(); + } const firstQuarterEnd = Math.ceil(numberOfTemplates / 4); @@ -23,13 +73,13 @@ test( `Total templates: ${numberOfTemplates}, Testing from 0 to ${firstQuarterEnd - 1} (first quarter)`, ); - // Collect template names first + // Collect template names first with stability checks for (let i = 0; i < firstQuarterEnd; i++) { - const exampleName = await page - .getByTestId("text_card_container") - .nth(i) - .getAttribute("role"); + // Ensure the specific template card is visible and stable + const templateCard = page.getByTestId("text_card_container").nth(i); + await expect(templateCard).toBeVisible({ timeout: 10000 }); + const exampleName = await templateCard.getAttribute("role"); templatesData.push({ index: i, name: exampleName }); } @@ -42,29 +92,62 @@ test( for (const template of templatesData) { console.log(`Testing template ${template.index}: ${template.name}`); - // Navigate directly to templates page - await page.goto("/"); + // Navigate directly to templates page with improved stability + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + + // Wait for main page to be fully loaded and stable await expect(page.getByTestId("mainpage_title")).toBeVisible({ timeout: 30000, }); - await page.getByTestId("new-project-btn").first().click(); - await page.getByTestId("side_nav_options_all-templates").click(); + await page.waitForLoadState("networkidle"); - // Wait for templates to load - await page.waitForSelector('[data-testid="text_card_container"]', { - timeout: 10000, - }); + // Sequential navigation with proper waiting (no Promise.all) + await safeNavigateAndClick(page, "new-project-btn"); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(500); // Brief pause between navigation steps - // Click on the specific template - await page.getByTestId("text_card_container").nth(template.index).click(); + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); + await page.waitForLoadState("networkidle"); - await page.waitForTimeout(1000); + // Ensure templates are fully loaded and stable before clicking + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); - await page.waitForSelector('[data-testid="div-generic-node"]', { - timeout: 15000, - }); + // Wait for the specific template to be visible and stable + const targetTemplate = page + .getByTestId("text_card_container") + .nth(template.index); + await expect(targetTemplate).toBeVisible({ timeout: 15000 }); + await page.waitForTimeout(300); // Ensure element is stable - if ((await page.getByTestId("update-all-button").count()) > 0) { + await targetTemplate.click(); + + // Wait for canvas to load properly with enhanced stability checks + await page.waitForLoadState("domcontentloaded"); + await page.waitForLoadState("networkidle", { timeout: 30000 }); + + // Wait for canvas elements to be visible and stable + await waitForElementStability( + page, + '[data-testid="div-generic-node"]', + 25000, + ); + + // Additional wait to ensure all canvas components are fully rendered + await page.waitForTimeout(1500); + + // Use auto-retrying assertion for checking outdated components + const updateButtonCount = await page + .getByTestId("update-all-button") + .count(); + if (updateButtonCount > 0) { console.error(` --------------------------------------------------------------------------------------- There's an outdated component on the basic template: ${template.name} @@ -72,6 +155,9 @@ test( `); numberOfOutdatedComponents++; } + + // Longer delay between template tests to prevent race conditions + await page.waitForTimeout(1000); } expect(numberOfOutdatedComponents).toBe(0); diff --git a/src/frontend/tests/core/integrations/starter-projects-shard3.spec.ts b/src/frontend/tests/core/integrations/starter-projects-shard3.spec.ts index 5049ca0150..45ddbb0581 100644 --- a/src/frontend/tests/core/integrations/starter-projects-shard3.spec.ts +++ b/src/frontend/tests/core/integrations/starter-projects-shard3.spec.ts @@ -1,6 +1,35 @@ import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; +// Helper function to ensure element stability before interaction +async function waitForElementStability( + page: any, + selector: string, + timeout = 5000, +) { + await page.waitForSelector(selector, { state: "visible", timeout }); + // Additional wait to ensure element is stable and not moving + await page.waitForTimeout(200); + // Use first() to avoid strict mode violation when multiple elements exist + await expect(page.locator(selector).first()).toBeVisible(); +} + +// Helper function to safely navigate with proper waiting +async function safeNavigateAndClick( + page: any, + testId: string, + waitForSelector?: string, +) { + const element = page.getByTestId(testId); + await expect(element).toBeVisible({ timeout: 15000 }); + await element.click(); + + if (waitForSelector) { + await page.waitForLoadState("domcontentloaded"); + await waitForElementStability(page, waitForSelector); + } +} + test( "user should be able to use second quarter of starter projects without any outdated components on the flow", { tag: ["@release", "@components"] }, @@ -10,12 +39,33 @@ test( const templatesData = []; let numberOfOutdatedComponents = 0; - // First, collect all template data - await page.getByTestId("side_nav_options_all-templates").click(); + // First, collect all template data with proper waiting + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); - const numberOfTemplates = await page + // Wait for templates to fully load and stabilize + await page.waitForLoadState("networkidle", { timeout: 30000 }); + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); + + // Ensure template count is stable by checking multiple times + let numberOfTemplates = await page .getByTestId("text_card_container") .count(); + await page.waitForTimeout(1000); // Allow any dynamic loading to complete + const verifyCount = await page.getByTestId("text_card_container").count(); + + // If counts don't match, wait a bit more and try again + if (numberOfTemplates !== verifyCount) { + await page.waitForTimeout(2000); + numberOfTemplates = await page.getByTestId("text_card_container").count(); + } const firstQuarterEnd = Math.ceil(numberOfTemplates / 4); const secondQuarterEnd = Math.ceil((numberOfTemplates * 2) / 4); @@ -24,13 +74,13 @@ test( `Total templates: ${numberOfTemplates}, Testing from ${firstQuarterEnd} to ${secondQuarterEnd - 1} (second quarter)`, ); - // Collect template names first + // Collect template names first with stability checks for (let i = firstQuarterEnd; i < secondQuarterEnd; i++) { - const exampleName = await page - .getByTestId("text_card_container") - .nth(i) - .getAttribute("role"); + // Ensure the specific template card is visible and stable + const templateCard = page.getByTestId("text_card_container").nth(i); + await expect(templateCard).toBeVisible({ timeout: 10000 }); + const exampleName = await templateCard.getAttribute("role"); templatesData.push({ index: i, name: exampleName }); } @@ -43,29 +93,62 @@ test( for (const template of templatesData) { console.log(`Testing template ${template.index}: ${template.name}`); - // Navigate directly to templates page - await page.goto("/"); + // Navigate directly to templates page with improved stability + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + + // Wait for main page to be fully loaded and stable await expect(page.getByTestId("mainpage_title")).toBeVisible({ timeout: 30000, }); - await page.getByTestId("new-project-btn").first().click(); - await page.getByTestId("side_nav_options_all-templates").click(); + await page.waitForLoadState("networkidle"); - // Wait for templates to load - await page.waitForSelector('[data-testid="text_card_container"]', { - timeout: 10000, - }); + // Sequential navigation with proper waiting (no Promise.all) + await safeNavigateAndClick(page, "new-project-btn"); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(500); // Brief pause between navigation steps - // Click on the specific template - await page.getByTestId("text_card_container").nth(template.index).click(); + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); + await page.waitForLoadState("networkidle"); - await page.waitForTimeout(1000); + // Ensure templates are fully loaded and stable before clicking + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); - await page.waitForSelector('[data-testid="div-generic-node"]', { - timeout: 15000, - }); + // Wait for the specific template to be visible and stable + const targetTemplate = page + .getByTestId("text_card_container") + .nth(template.index); + await expect(targetTemplate).toBeVisible({ timeout: 15000 }); + await page.waitForTimeout(300); // Ensure element is stable - if ((await page.getByTestId("update-all-button").count()) > 0) { + await targetTemplate.click(); + + // Wait for canvas to load properly with enhanced stability checks + await page.waitForLoadState("domcontentloaded"); + await page.waitForLoadState("networkidle", { timeout: 30000 }); + + // Wait for canvas elements to be visible and stable + await waitForElementStability( + page, + '[data-testid="div-generic-node"]', + 25000, + ); + + // Additional wait to ensure all canvas components are fully rendered + await page.waitForTimeout(1500); + + // Use auto-retrying assertion for checking outdated components + const updateButtonCount = await page + .getByTestId("update-all-button") + .count(); + if (updateButtonCount > 0) { console.error(` --------------------------------------------------------------------------------------- There's an outdated component on the basic template: ${template.name} @@ -73,6 +156,9 @@ test( `); numberOfOutdatedComponents++; } + + // Longer delay between template tests to prevent race conditions + await page.waitForTimeout(1000); } expect(numberOfOutdatedComponents).toBe(0); diff --git a/src/frontend/tests/core/integrations/starter-projects-shard4.spec.ts b/src/frontend/tests/core/integrations/starter-projects-shard4.spec.ts index 19d6cffe5a..0c968f64de 100644 --- a/src/frontend/tests/core/integrations/starter-projects-shard4.spec.ts +++ b/src/frontend/tests/core/integrations/starter-projects-shard4.spec.ts @@ -1,6 +1,35 @@ import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; +// Helper function to ensure element stability before interaction +async function waitForElementStability( + page: any, + selector: string, + timeout = 5000, +) { + await page.waitForSelector(selector, { state: "visible", timeout }); + // Additional wait to ensure element is stable and not moving + await page.waitForTimeout(200); + // Use first() to avoid strict mode violation when multiple elements exist + await expect(page.locator(selector).first()).toBeVisible(); +} + +// Helper function to safely navigate with proper waiting +async function safeNavigateAndClick( + page: any, + testId: string, + waitForSelector?: string, +) { + const element = page.getByTestId(testId); + await expect(element).toBeVisible({ timeout: 15000 }); + await element.click(); + + if (waitForSelector) { + await page.waitForLoadState("domcontentloaded"); + await waitForElementStability(page, waitForSelector); + } +} + test( "user should be able to use fourth quarter of starter projects without any outdated components on the flow", { tag: ["@release", "@components"] }, @@ -10,12 +39,33 @@ test( const templatesData = []; let numberOfOutdatedComponents = 0; - // First, collect all template data - await page.getByTestId("side_nav_options_all-templates").click(); + // First, collect all template data with proper waiting + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); - const numberOfTemplates = await page + // Wait for templates to fully load and stabilize + await page.waitForLoadState("networkidle", { timeout: 30000 }); + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); + + // Ensure template count is stable by checking multiple times + let numberOfTemplates = await page .getByTestId("text_card_container") .count(); + await page.waitForTimeout(1000); // Allow any dynamic loading to complete + const verifyCount = await page.getByTestId("text_card_container").count(); + + // If counts don't match, wait a bit more and try again + if (numberOfTemplates !== verifyCount) { + await page.waitForTimeout(2000); + numberOfTemplates = await page.getByTestId("text_card_container").count(); + } const thirdQuarterEnd = Math.ceil((numberOfTemplates * 3) / 4); @@ -23,13 +73,13 @@ test( `Total templates: ${numberOfTemplates}, Testing from ${thirdQuarterEnd} to ${numberOfTemplates - 1} (fourth quarter)`, ); - // Collect template names first + // Collect template names first with stability checks for (let i = thirdQuarterEnd; i < numberOfTemplates; i++) { - const exampleName = await page - .getByTestId("text_card_container") - .nth(i) - .getAttribute("role"); + // Ensure the specific template card is visible and stable + const templateCard = page.getByTestId("text_card_container").nth(i); + await expect(templateCard).toBeVisible({ timeout: 10000 }); + const exampleName = await templateCard.getAttribute("role"); templatesData.push({ index: i, name: exampleName }); } @@ -42,29 +92,62 @@ test( for (const template of templatesData) { console.log(`Testing template ${template.index}: ${template.name}`); - // Navigate directly to templates page - await page.goto("/"); + // Navigate directly to templates page with improved stability + await page.goto("/", { waitUntil: "networkidle", timeout: 30000 }); + + // Wait for main page to be fully loaded and stable await expect(page.getByTestId("mainpage_title")).toBeVisible({ timeout: 30000, }); - await page.getByTestId("new-project-btn").first().click(); - await page.getByTestId("side_nav_options_all-templates").click(); + await page.waitForLoadState("networkidle"); - // Wait for templates to load - await page.waitForSelector('[data-testid="text_card_container"]', { - timeout: 10000, - }); + // Sequential navigation with proper waiting (no Promise.all) + await safeNavigateAndClick(page, "new-project-btn"); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(500); // Brief pause between navigation steps - // Click on the specific template - await page.getByTestId("text_card_container").nth(template.index).click(); + await safeNavigateAndClick( + page, + "side_nav_options_all-templates", + '[data-testid="text_card_container"]', + ); + await page.waitForLoadState("networkidle"); - await page.waitForTimeout(1000); + // Ensure templates are fully loaded and stable before clicking + await waitForElementStability( + page, + '[data-testid="text_card_container"]', + 20000, + ); - await page.waitForSelector('[data-testid="div-generic-node"]', { - timeout: 15000, - }); + // Wait for the specific template to be visible and stable + const targetTemplate = page + .getByTestId("text_card_container") + .nth(template.index); + await expect(targetTemplate).toBeVisible({ timeout: 15000 }); + await page.waitForTimeout(300); // Ensure element is stable - if ((await page.getByTestId("update-all-button").count()) > 0) { + await targetTemplate.click(); + + // Wait for canvas to load properly with enhanced stability checks + await page.waitForLoadState("domcontentloaded"); + await page.waitForLoadState("networkidle", { timeout: 30000 }); + + // Wait for canvas elements to be visible and stable + await waitForElementStability( + page, + '[data-testid="div-generic-node"]', + 25000, + ); + + // Additional wait to ensure all canvas components are fully rendered + await page.waitForTimeout(1500); + + // Use auto-retrying assertion for checking outdated components + const updateButtonCount = await page + .getByTestId("update-all-button") + .count(); + if (updateButtonCount > 0) { console.error(` --------------------------------------------------------------------------------------- There's an outdated component on the basic template: ${template.name} @@ -72,6 +155,9 @@ test( `); numberOfOutdatedComponents++; } + + // Longer delay between template tests to prevent race conditions + await page.waitForTimeout(1000); } expect(numberOfOutdatedComponents).toBe(0);