# Test info

- Name: Case already exists >> CASE_033 - Add and manage businesses in case's detail
- Location: /root/code/portal-automation-test/tests/case/crud/crud.spec.ts:386:7

# Error details

```
Error: locator.waitFor: Test timeout of 80000ms exceeded.
Call log:
  - waiting for locator('xpath=(//a[text()=\'872135\'])[1] | (//span[text()=\'872135\'])[1][not(//a[text()=\'872135\'])]').first() to be visible

    at CasePage.getDetailCase (/root/code/portal-automation-test/pom/dashboard.page.ts:461:65)
    at /root/code/portal-automation-test/tests/case/crud/crud.spec.ts:140:7
    at /root/code/portal-automation-test/tests/case/crud/crud.spec.ts:135:5
```

# Page snapshot

```yaml
- complementary:
  - img
  - link "WellCare Pharmacy12321@@@@ Portal QA":
    - /url: /
    - paragraph: WellCare Pharmacy12321@@@@
    - paragraph: Portal QA
  - list:
    - listitem:
      - link "Home":
        - /url: https://qa.loprx.com
        - img
        - text: Home
    - listitem:
      - link "Case":
        - /url: https://qa.loprx.com/cases
        - img
        - text: Case
    - listitem:
      - link "Request":
        - /url: https://qa.loprx.com/requests
        - img
        - text: Request
    - listitem:
      - link "Patient":
        - /url: https://qa.loprx.com/clients
        - img
        - text: Patient
    - listitem:
      - button "Business/Contact":
        - img
        - text: Business/Contact
    - listitem:
      - button "Inbox/Sent":
        - img
        - text: Inbox/Sent
    - listitem:
      - button "Settings":
        - img
        - text: Settings
  - list:
    - listitem:
      - button "Shared with me":
        - img
        - text: Shared with me
- banner:
  - button [disabled]:
    - img
  - button:
    - img
  - img
  - text: Tri01 Duc shared details regarding Auto_YXQ Test Letter of Protection. Read it now. 10 hours ago
  - img
  - text: Son HaiBon Phan assigned a request to you 10 hours ago
  - img
  - text: Son HaiBon Phan assigned a case to you 11 hours ago
  - img
  - img
  - img
  - paragraph: No notifications yet
  - paragraph: When you get notifications, they'll show up here
  - button "Refresh"
  - list:
    - listitem:
      - img
      - paragraph: Pause notifications...
      - list:
        - listitem: For 30 minutes
        - listitem: For 1 hour
        - listitem: For 2 hours
        - listitem: Until tomorrow
    - listitem:
      - img
    - listitem:
      - img
    - listitem:
      - img
    - listitem:
      - img
    - listitem:
      - img
    - listitem:
      - img
  - img
  - button "0 Cart":
    - img
    - text: 0 Cart
  - text: Feedback
  - img
  - link "Open user menu":
    - /url: "#"
    - text: Minh
- img
- text: Cases
- button "New Case":
  - img
  - text: New Case
- img
- heading "Manage" [level=2]:
  - text: Manage
  - img
- img
- text: Show
- textbox: "10"
- text: entries
- button "Bulk Edit":
  - img
  - text: Bulk Edit
- button "Filter":
  - img
  - text: Filter
- img
- textbox "Search...": "872135"
- img
- text: No matching results found. Try again.
```

# Test source

```ts
  361 |     async openThreedotMenu(menuName: string, threeDotLocator: Locator | null = null, buttonHref?: boolean) {
  362 |         const tableHeadingLoc = this.dashboardLoc.listing.tableHeading;
  363 |         if (threeDotLocator) {
  364 |             await threeDotLocator.click();
  365 |         } else {
  366 |             await tableHeadingLoc.btnThreedot.click();
  367 |         }
  368 |         await this.waitForSecond(1.5);
  369 |         buttonHref ? await tableHeadingLoc.dropdownMenuWithATag(menuName).click() : await tableHeadingLoc.dropdownMenu(menuName).first().click();
  370 |     }
  371 |
  372 |     async pinItem(rowIndex: number) {
  373 |         await this.dashboardLoc.table.pinItem(rowIndex).click();
  374 |     }
  375 |     async getTableHeadingOrder(name: string): Promise<number> {
  376 |         const headings = await this.dashboardLoc.table.container.locator('thead').locator('th').all();
  377 |         for (let i = 0; i < headings.length; i++) {
  378 |             const text = await headings[i].textContent();
  379 |             if (text?.trim() === name) {
  380 |                 return i;
  381 |             }
  382 |         }
  383 |         return -1;
  384 |     }
  385 |
  386 |     async getTableData(columnIndex: number, haveVisibleRow?: boolean, isTableBulkEdit?: boolean): Promise<string[]> {
  387 |         const rows = isTableBulkEdit ? await this.page.locator("//table[contains(@class, 'card-table-bulk')]").locator('tbody').locator('tr').all() : await this.dashboardLoc.table.container.locator('tbody').locator('tr').all()
  388 |         // const rows = await this.dashboardLoc.table.container.locator('tbody').locator('tr').all();
  389 |         const rowsLength = !haveVisibleRow ? rows.length : 10;
  390 |         const data: string[] = [];
  391 |         for (let i = 0; i < rowsLength; i++) {
  392 |             const cells = await rows[i].locator('td').all();
  393 |             const cellText = await cells[columnIndex].textContent();
  394 |             const trimmedText = cellText?.trim() || '';
  395 |             if (trimmedText === 'Missing') {
  396 |                 data.push('--');
  397 |             } else {
  398 |                 data.push(trimmedText);
  399 |             }
  400 |         }
  401 |         return data;
  402 |     }
  403 |
  404 |     async getDataInTable(displayName: string, propertyName: string, tableApiUrl: string, haveVisibleRow?: boolean): Promise<TableData> {
  405 |         // Get data from table
  406 |         const order = await this.getTableHeadingOrder(displayName);
  407 |         expect(order, `Column ${displayName} should be visible`).toBeGreaterThan(-1);
  408 |         await this.waitForSecond(3); // TODO: refactor to wait api completed or table rendered
  409 |         const data = await this.getTableData(order, haveVisibleRow);
  410 |
  411 |         // Compare with data from stat
  412 |         const rawData = await this.page.request.get(tableApiUrl);
  413 |         const apiData = await rawData.json();
  414 |         const apiDataValues = getPropertyValues(apiData.data, propertyName, modifierNullToDash);
  415 |         console.log(apiDataValues);
  416 |         console.log(data);
  417 |         return { dataApi: apiDataValues, dataUI: data };
  418 |     }
  419 |
  420 |     async getTableRowIndexByColumnValue(columnIndex: number, value: string): Promise<number> {
  421 |         const rows = await this.dashboardLoc.table.container.locator('tbody').locator('tr').all();
  422 |         for (let i = 0; i < rows.length; i++) {
  423 |             const cells = await rows[i].locator('td').all();
  424 |             const cellText = await cells[columnIndex].textContent();
  425 |             if (cellText?.trim() === value) {
  426 |                 return i;
  427 |             }
  428 |         }
  429 |         return -1;
  430 |     }
  431 |
  432 |     async search(keyword: string) {
  433 |         await this.dashboardLoc.search.inputSearch.fill(keyword);
  434 |         await this.dashboardLoc.search.inputSearch.press('Enter');
  435 |     }
  436 |
  437 |     async getTableRowCount(): Promise<number> {
  438 |         return await this.dashboardLoc.table.container.locator('tbody').locator('tr').count();
  439 |     }
  440 |
  441 |     async resetToDefaultColumn(cusomizeColumn: any[]): Promise<void> {
  442 |         const customizedColumn = 'Customized Columns';
  443 |         const customThreeDotLocator = this.genLoc("(//div[contains(@class, 'card-header')]//span[contains(@class, 'cursor-pointer')])[2]");
  444 |         const customizedColumns = cusomizeColumn.filter((column: { default: any; }) => !column.default);
  445 |         for (let i = 0; i < customizedColumns.length; i++) {
  446 |             await this.openThreedotMenu(customizedColumn, customThreeDotLocator);
  447 |             await this.dashboardLoc.listing.popup.customizedColumn.columnCheckbox(customizedColumns[i].name).setChecked(false);
  448 |             await this.dashboardLoc.listing.popup.customizedColumn.btnApply.click();
  449 |         }
  450 |     }
  451 |
  452 |     async getDetailCase(caseID: string, archiveBy?: string): Promise<void> {
  453 |         if (archiveBy) {
  454 |             await this.dashboardLoc.search.filter.btn.click();
  455 |             await this.dashboardLoc.search.filter.popup.inputField("is_archived").click();
  456 |             await this.dashboardLoc.search.filter.popup.optionInput(archiveBy).click();
  457 |             await this.dashboardLoc.search.filter.popup.btnApply.click();
  458 |         }
  459 |         await this.dashboardLoc.search.inputSearch.fill(caseID);
  460 |         await this.waitForSecond(2);
> 461 |         await this.dashboardLoc.table.itemInRow(caseID).first().waitFor({ state: 'visible' });
      |                                                                 ^ Error: locator.waitFor: Test timeout of 80000ms exceeded.
  462 |         await this.dashboardLoc.table.itemInRow(caseID).first().click({ force: true });
  463 |     }
  464 |
  465 |     async changeUser(username: string, password: string, page: Page): Promise<void> {
  466 |         let loginPage = new LoginPage(page);
  467 |         await this.logout();
  468 |         await page.context().clearCookies();
  469 |         await page.evaluate(() => {
  470 |             localStorage.clear();
  471 |             sessionStorage.clear();
  472 |         });
  473 |         await page.reload();
  474 |         await loginPage.open();
  475 |         await loginPage.login(username, password);
  476 |     }
  477 |
  478 |     async checkNotification(type: string, gotoFirstNoti?: boolean): Promise<void> {
  479 |         try {
  480 |             const closeButton = await this.page.waitForSelector(
  481 |                 `//div[@class='notification-popup-latest-wrapper displayed']/descendant::div[@class='close-btn']`,
  482 |                 { state: 'visible', timeout: 2000 }
  483 |             );
  484 |             await closeButton.click();
  485 |         } catch (error) {
  486 |             console.log('No notification popup appeared');
  487 |         }
  488 |
  489 |         await this.dashboardLoc.notification.btnNotification.waitFor({ state: 'visible' });
  490 |         await this.dashboardLoc.notification.btnNotification.click({ force: true });
  491 |         await this.dashboardLoc.notification.notificationType(type).click();
  492 |         if (gotoFirstNoti) {
  493 |             await this.dashboardLoc.notification.listNotiReminder.first().click();
  494 |         }
  495 |     }
  496 |
  497 |     async addMultipleTags(tagCount: number, tagPrefix: string = "test"): Promise<void> {
  498 |         await this.dashboardLoc.tag.boxTag.click();
  499 |         for (let i = 0; i < tagCount; i++) {
  500 |             await this.dashboardLoc.tag.inputTag.first().fill(`${tagPrefix}${i}`);
  501 |             await this.dashboardLoc.tag.inputTag.first().press("Enter");
  502 |         }
  503 |     }
  504 |
  505 |     async saveTags(): Promise<void> {
  506 |         await this.dashboardLoc.tag.btnSaveTag.click();
  507 |         await this.waitForSecond(1);
  508 |     }
  509 |
  510 |     async getTagCount(): Promise<number> {
  511 |         return (await this.dashboardLoc.tag.listTagDetail.all()).length;
  512 |     }
  513 |
  514 |     async removeOneTag(): Promise<void> {
  515 |         await this.dashboardLoc.tag.listRemoveTag.first().click();
  516 |     }
  517 |
  518 |     async clearAllTags(): Promise<void> {
  519 |         await this.dashboardLoc.tag.listTagDetail.first().click();
  520 |         await this.dashboardLoc.tag.btnClearAllTag.waitFor({ state: 'visible' });
  521 |         await this.waitForSecond(1);
  522 |         await this.dashboardLoc.tag.btnClearAllTag.click({ force: true });
  523 |     }
  524 |
  525 |     async showEntries(entries: Number) {
  526 |         const inputShowSelector = `//input[@id='input_per_page']`;
  527 |         await this.page.locator(inputShowSelector).click();
  528 |         await this.page.locator(inputShowSelector).fill(entries.toString());
  529 |         await this.page.waitForLoadState("domcontentloaded");
  530 |     }
  531 |
  532 |     /**
  533 |      * Universal bulk edit verification function with dynamic parameters
  534 |      * Optimized for reuse across multiple test cases and page types
  535 |      * @param tabName - The tab name to click and verify ("Contacts", "Cases", "Request", etc.)
  536 |      * @param entryCount - Number of entries to show in the table
  537 |      * @param detailPageName - Name of the detail page for back navigation verification
  538 |      * @param shouldNavigateBack - Whether to navigate back after verification (default: true)
  539 |      */
  540 |     async verifyBulkEditForTab(
  541 |         tabName: string,
  542 |         entryCount: number,
  543 |         detailPageName: string,
  544 |         shouldNavigateBack: boolean = true
  545 |     ): Promise<void> {
  546 |         // Navigate to tab and show entries
  547 |         await this.dashboardLoc.detail.tabName(tabName).waitFor({ state: 'visible' });
  548 |         await this.waitForSecond(1.5);
  549 |         await this.dashboardLoc.detail.tabName(tabName).click({ force: true });
  550 |         await this.showEntries(entryCount);
  551 |         await this.waitForSecond(2);
  552 |
  553 |         // Get original table data
  554 |         const originalData = await this.getTableData(0);
  555 |
  556 |         // Click Bulk Edit and verify
  557 |         await this.dashboardLoc.buttonByText("Bulk Edit").click();
  558 |         await this.dashboardLoc.common.spanText("Bulk Edit").first().waitFor({ state: 'visible' });
  559 |         await this.waitForSecond(2);
  560 |
  561 |         // Get bulk edit table data and compare
```