Repository: department-of-veterans-affairs/next-build
Pull request #979: Fixing Pagination Regex
Author: login: moanu404
Created: 2025-04-16T21:03:27Z | Merged: 2025-04-24T22:08:31Z

# Description
Fixes regex to correctly match multi-digit page numbers (e.g. page-10), not just single digits.
## Generated description

This pull request improves handling of double-digit page numbers for listing pages and press release pages, ensuring proper functionality in both tests and application logic. Key changes include the addition of new test cases, updates to slug definitions, and a bug fix in the page number regex.

### Test Enhancements:
* Added tests in `playwright/tests/pressReleaseListing.spec.ts` to validate navigation and item counts for double-digit pages in press release listings.
* Added tests in `playwright/tests/storyListing.spec.ts` to validate navigation and item counts for double-digit pages in story listings.
* Extended tests in `src/lib/drupal/listingPages.test.ts` to cover handling of double-digit page numbers for both story listings and press release listings, including pages 10, 13, and 20.

### Code Updates:
* Updated `src/lib/drupal/listingPages.test.ts` to define additional slug constants for double-digit pages in story and press release listings.
* Fixed a regex bug in `src/lib/drupal/listingPages.ts` to correctly match double-digit page numbers in slugs.
## Ticket

<!--
https://github.com/department-of-veterans-affairs/va.gov-cms/issues/20731-->

Closes [20731](https://github.com/department-of-veterans-affairs/va.gov-cms/issues/20731)

## Developer Task

```md
- [ ] PR submitted against the `main` branch of `next-build`.
- [ ] Link to the issue that this PR addresses (if applicable).
- [ ] Define all changes in your PR and note any changes that could potentially be breaking changes.
- [ ] PR includes steps to test your changes and links to these changes in the Tugboat preview (if applicable).
- [ ] Provided before and after screenshots of your changes (if applicable).
- [ ] Alerted the #next-build Slack channel to request a PR review.
- [ ] You understand that once approved, you are responsible for merging your changes into `main`. (Note that changes to `main` will move automatically into production.)
```

## Testing Steps

- Load up local host to http://localhost:3999/washington-dc-health-care/stories/page-10/
- Ensure all listings do not show up
- click page 13 and make sure you don't jump to page 3 
- check tugboat for the same behaviour
- run unit tests
## QA steps

What needs to be checked to prove this works?
What needs to be checked to prove it didn't break any related things?
What variations of circumstances (users, actions, values) need to be checked?

## Screenshots

Before:
<img width="1290" alt="image" src="https://github.com/user-attachments/assets/9e1dfeca-bce5-49a6-9505-b01fcadd0813" />
<img width="1292" alt="image" src="https://github.com/user-attachments/assets/63d4bc62-24d9-4de9-a7b6-c7679db170a3" />

After:
<img width="1275" alt="image" src="https://github.com/user-attachments/assets/feeba555-54f9-4bbd-9100-efc3f125cb93" />

<img width="1276" alt="image" src="https://github.com/user-attachments/assets/8f1fcc66-f86a-460a-be1b-d753e4f1146d" />


## Is this PR blocked by another PR?

- Add the `DO NOT MERGE` label
- Add links to additional PRs here:

---

## Reviewer

### Reviewing a PR

This section lists items that need to be checked or updated when making changes to this repository.

## Standard Checks

```md
- [ ] Code Quality: Readabilty, Naming Conventions, Consistency, Reusability
- [ ] Test Coverage: 80% coverage
- [ ] Functionality: Change functions as expected with no additional bugs
- [ ] Performance: Code does not introduce performance issues
- [ ] Documentation: Changes are documented in their respective README.md files
- [ ] Security: Packages have been approved in the TRM
```

## Merging a Layout

When merging a layout, you must ensure that the content type has been turned on for `next-build` in the [.tugboat.env](../envs/.tugboat.env). This method mocks the CMS flag that must be turned on for a layout to be included in the build.

The layout component and matching resource type should be included in the [slug.tsx](../src/pages/[[...slug]].tsx), so that it can reviewed. Including a component in the slug.tsx does not mean a page will be viewable in production only on the tugboat for the branch.

When a layout is merged to main and approved for deployment, the prod CMS will turn the toggle on for the resource type. 

The status of layouts should be kept up to date inside [templates.md](../READMEs/templates.md). This includes QA progress, development progress, etc. A link should be provided for where testing can occur.


Diff:
diff --git a/playwright/tests/pressReleaseListing.spec.ts b/playwright/tests/pressReleaseListing.spec.ts
index 39e414a06..cbbbd3a8e 100644
--- a/playwright/tests/pressReleaseListing.spec.ts
+++ b/playwright/tests/pressReleaseListing.spec.ts
@@ -11,7 +11,7 @@ test.describe('pressReleaseListing', () => {
     )
   })
 
-  test('Press Release Listing page should be paginated if there are ore than 10 stories', async ({
+  test('Press Release Listing page should be paginated if there are more than 10 stories', async ({
     page,
   }) => {
     await page.goto('/southern-nevada-health-care/news-releases')
@@ -28,6 +28,33 @@ test.describe('pressReleaseListing', () => {
     await expect(nextPageLink).toBeEnabled()
   })
 
+  test('Press Release Listing should handle double-digit page numbers correctly', async ({
+    page,
+  }) => {
+    await page.goto('/saginaw-health-care/news-releases')
+    for (let i = 1; i < 10; i++) {
+      const nextPageLink = page.getByLabel('Next page')
+      // Confirm it's visible before clicking
+      await expect(nextPageLink).toBeVisible({ timeout: 5000 })
+      // Wait for both the click and URL change — works with client-side routing
+      await Promise.all([
+        page.waitForURL(new RegExp(`/page-${i + 1}/`), { timeout: 10000 }),
+        nextPageLink.click(),
+      ])
+    }
+    // Confirm we’re on page 10
+    await expect(page).toHaveURL(/\/page-10\//)
+    const pressReleaseItems = page.locator('.usa-unstyled-list li')
+    await expect(pressReleaseItems).toHaveCount(10)
+    // Navigate to page 11
+    const nextPageLink = page.getByLabel('Next page')
+    await Promise.all([
+      page.waitForURL(/\/page-11\//, { timeout: 10000 }),
+      nextPageLink.click(),
+    ])
+    await expect(page).toHaveURL(/\/page-11\//)
+  })
+
   test('Should render without a11y errors', async ({
     page,
     makeAxeBuilder,
diff --git a/playwright/tests/storyListing.spec.ts b/playwright/tests/storyListing.spec.ts
index 634dcf7c1..7c308f219 100644
--- a/playwright/tests/storyListing.spec.ts
+++ b/playwright/tests/storyListing.spec.ts
@@ -26,6 +26,34 @@ test.describe('Story Listing', () => {
     await expect(nextPageLink).toBeEnabled()
   })
 
+  test('Story Listing should handle double-digit page numbers correctly', async ({
+    page,
+  }) => {
+    await page.goto('/washington-dc-health-care/stories')
+    for (let i = 1; i < 10; i++) {
+      const nextPageLink = page.getByLabel('Next page')
+      // Ensure it's visible and interactable before click
+      await expect(nextPageLink).toBeVisible({ timeout: 5000 })
+      // Wait for click + navigation concurrently
+      await Promise.all([
+        page.waitForURL(new RegExp(`/page-${i + 1}/`), { timeout: 10000 }),
+        nextPageLink.click(),
+      ])
+    }
+    // Now on page 10
+    await expect(page).toHaveURL(/\/page-10\//)
+    const storyItems = page.locator('.usa-unstyled-list li')
+    await expect(storyItems).toHaveCount(10)
+    // Go to page 11
+    const nextPageLink = page.getByLabel('Next page')
+    await Promise.all([
+      page.waitForURL(/\/page-11\//, { timeout: 10000 }),
+      nextPageLink.click(),
+    ])
+    await expect(page).toHaveURL(/\/page-11\//)
+    await expect(storyItems).toHaveCount(10)
+  })
+
   test('Should render without a11y errors', async ({
     page,
     makeAxeBuilder,
diff --git a/src/lib/drupal/listingPages.test.ts b/src/lib/drupal/listingPages.test.ts
index 1a710514a..982368458 100644
--- a/src/lib/drupal/listingPages.test.ts
+++ b/src/lib/drupal/listingPages.test.ts
@@ -7,8 +7,26 @@ import {
 import { slugToPath } from '@/lib/utils/slug'
 
 const listingPageFirstPageSlug = ['some-health-care', 'stories']
-
 const listingPageSecondPageSlug = ['some-health-care', 'stories', 'page-2']
+const listingPageTenthPageSlug = ['some-health-care', 'stories', 'page-10']
+const listingPageTwentiethPageSlug = ['some-health-care', 'stories', 'page-20']
+
+const pressReleaseFirstPageSlug = ['some-health-care', 'news-releases']
+const pressReleaseSecondPageSlug = [
+  'some-health-care',
+  'news-releases',
+  'page-2',
+]
+const pressReleaseTenthPageSlug = [
+  'some-health-care',
+  'news-releases',
+  'page-10',
+]
+const pressReleaseThirteenthPageSlug = [
+  'some-health-care',
+  'news-releases',
+  'page-13',
+]
 
 const nonListingPageSlug = ['some-health-care', 'stories', 'story-title']
 
@@ -71,6 +89,92 @@ describe('getListingPageStaticPropsContext', () => {
     })
   })
 
+  test('should properly handle double-digit page number (page 10)', () => {
+    const context = {
+      params: {
+        slug: listingPageTenthPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(listingPageFirstPageSlug),
+      page: 10,
+    })
+  })
+
+  test('should properly handle double-digit page number (page 20)', () => {
+    const context = {
+      params: {
+        slug: listingPageTwentiethPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(listingPageFirstPageSlug),
+      page: 20,
+    })
+  })
+})
+
+describe('getListingPageStaticPropsContext for Press Releases', () => {
+  test('should properly handle first page', () => {
+    const context = {
+      params: {
+        slug: pressReleaseFirstPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(pressReleaseFirstPageSlug),
+      page: 1,
+    })
+  })
+
+  test('should properly handle single-digit page number', () => {
+    const context = {
+      params: {
+        slug: pressReleaseSecondPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(pressReleaseFirstPageSlug),
+      page: 2,
+    })
+  })
+
+  test('should properly handle double-digit page number (page 10)', () => {
+    const context = {
+      params: {
+        slug: pressReleaseTenthPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(pressReleaseFirstPageSlug),
+      page: 10,
+    })
+  })
+
+  test('should properly handle double-digit page number (page 13)', () => {
+    const context = {
+      params: {
+        slug: pressReleaseThirteenthPageSlug,
+      },
+    }
+    const result = getListingPageStaticPropsContext(context)
+    expect(result).toStrictEqual({
+      isListingPage: true,
+      firstPagePath: slugToPath(pressReleaseFirstPageSlug),
+      page: 13,
+    })
+  })
+
   test('should return null/false listing values when not listing page', () => {
     const context = {
       params: {
diff --git a/src/lib/drupal/listingPages.ts b/src/lib/drupal/listingPages.ts
index 2e3f9a551..44e5b50ca 100644
--- a/src/lib/drupal/listingPages.ts
+++ b/src/lib/drupal/listingPages.ts
@@ -230,7 +230,7 @@ export function getListingPageStaticPropsContext(
     // EVENT_LISTING pages should only generate one page.
     slug[1] !==
       LISTING_RESOURCE_TYPE_URL_SEGMENTS[RESOURCE_TYPES.EVENT_LISTING] &&
-    slug[2].match(/^page-(\d)+$/)
+    slug[2].match(/^page-(\d+)$/)
   const page = isSlugSubsequentListingPage
     ? parseInt(isSlugSubsequentListingPage[1])
     : null


Reviews:
user: login: moanu404
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T22:18:10Z
user: login: brianseek
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:17:37Z
user: login: brianseek
body: Looks good! working locally and in tugboat. Nice work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:18:06Z
user: login: cvalarida
body: Looks great!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:57:42Z

Review comments:
diff_hunk: @@ -71,6 +89,92 @@ describe('getListingPageStaticPropsContext', () => {
     })
   })
 
+  test('should properly handle double-digit page number (page 10)', () => {
path: src/lib/drupal/listingPages.test.ts
user: login: moanu404
body: Definitely used copilot for this so let me know if something looks funny~ 🤖 
created_at: 2025-04-16T22:18:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +89,92 @@ describe('getListingPageStaticPropsContext', () => {
     })
   })
 
+  test('should properly handle double-digit page number (page 10)', () => {
path: src/lib/drupal/listingPages.test.ts
user: login: brianseek
body: copilot has been pretty great for setting up tests, these make sense to me
created_at: 2025-04-17T17:17:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,7 +230,7 @@ export function getListingPageStaticPropsContext(
     // EVENT_LISTING pages should only generate one page.
     slug[1] !==
       LISTING_RESOURCE_TYPE_URL_SEGMENTS[RESOURCE_TYPES.EVENT_LISTING] &&
-    slug[2].match(/^page-(\d)+$/)
+    slug[2].match(/^page-(\d+)$/)
path: src/lib/drupal/listingPages.ts
user: login: cvalarida
body: 🤣 I laughed entirely too much at this.
created_at: 2025-04-17T17:39:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +89,92 @@ describe('getListingPageStaticPropsContext', () => {
     })
   })
 
+  test('should properly handle double-digit page number (page 10)', () => {
path: src/lib/drupal/listingPages.test.ts
user: login: cvalarida
body: Looks solid. If you wanted to reduce the line count, you could use a `test.each()`, but 🤷 
created_at: 2025-04-17T17:57:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: laflannery
created_at: 2025-04-16T22:38:32Z
body: Looks good @moanu404 I tested quite a few listings and they all are working properly now!
author_association: CONTRIBUTOR
user: login: brianseek
created_at: 2025-04-17T17:20:42Z
body: The new playwrite tests looked like they were timing out, tried to re-run with no luck, so not sure what is up with that.
author_association: CONTRIBUTOR
user: login: moanu404
created_at: 2025-04-17T18:13:22Z
body: > The new playwrite tests looked like they were timing out, tried to re-run with no luck, so not sure what is up with that.

I nearly forgot those were not supposed to be red anymore lol I'll take a look
author_association: CONTRIBUTOR
user: login: moanu404
created_at: 2025-04-24T22:07:24Z
body: > The new playwrite tests looked like they were timing out, tried to re-run with no luck, so not sure what is up with that.

Finally wrangled them and got them to pass phew! green and all good to merge now! 
author_association: CONTRIBUTOR