# Test info

- Name: UI case for request tests >> REQ_029 - Verify UI when turn off all Notification
- Location: /root/code/portal-automation-test/tests/request/ui.spec.ts:113:7

# Error details

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

    at RequestPage.getDetailCase (/root/code/portal-automation-test/pom/dashboard.page.ts:511:65)
    at /root/code/portal-automation-test/tests/request/ui.spec.ts:117:7
    at /root/code/portal-automation-test/tests/request/ui.spec.ts:116:5
```

# Page snapshot

```yaml
- complementary:
  - img
  - link "(Portal-Auto-Lawyer) Portal QA":
    - /url: /
    - paragraph: (Portal-Auto-Lawyer)
    - 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 "Client":
        - /url: https://qa.loprx.com/clients
        - img
        - text: Client
    - listitem:
      - button "Business/Contact":
        - img
        - text: Business/Contact
    - listitem:
      - button "Inbox/Sent":
        - img
        - text: Inbox/Sent
    - listitem:
      - button "Settings":
        - img
        - text: Settings
  - list:
    - listitem:
      - link "Shared with me":
        - /url: https://qa.loprx.com/shared/requests
        - img
        - text: Shared with me
- banner:
  - button:
    - img
  - button [disabled]:
    - img
  - img
  - text: Link Case First shared details about Third Party Letter of Protection 1 week ago
  - img
  - text: What did Tri01 Duc say? Their response to Test Request Letter of Protection is waiting. 4 days ago
  - img
  - text: Tri01 Duc shared some information about Test Request Letter of Protection. Find out what it says. 4 days ago
  - img
  - text: What did Tri01 Duc say? Their response to Test Request Letter of Protection is waiting. 4 days ago
  - img
  - img
  - textbox "Search..."
  - 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 "4 Cart":
    - img
    - text: 4 Cart
  - text: Feedback
  - img
  - link "Open user menu":
    - /url: "#"
    - text: Minh
- img
- text: Requests
- button "New Request":
  - img
  - text: New Request
- text: Tips
- img
- text: Think of the request section as your case file's magnifying glass. Here, you can zoom in on specific details and find exactly what you're looking for. Plus, you get a quick snapshot of each request's status - what's been requested, how it's progressing, and if it's been resolved. Click on
- link "\"New Request\"":
  - /url: /requests/create
- text: to try it out! Active 6081 Reference 3 Archived 3
- img
- text: Show
- textbox: "10"
- text: entries
- button "4 Quick Submit"
- button "Bulk Edit":
  - img
  - text: Bulk Edit
- button "Filter":
  - img
  - text: Filter
- img
- textbox "Search...": "679105"
- img
- text: No matching results found. Try again.
```

# Test source

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