next.js/test/development/app-dir/hmr-dep-accept/hmr-dep-accept.test.ts
hmr-dep-accept.test.ts279 lines9.2 KB
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'

describe('hmr-dep-accept', () => {
  const { next, isTurbopack, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  // Dependency accept/decline requires Turbopack dev mode
  const itTurbopackDev = isTurbopack && isNextDev ? it : it.skip

  describe('dependency accept', () => {
    itTurbopackDev(
      'parent accepts child dependency update without re-evaluating',
      async () => {
        const browser = await next.browser('/dep-accept')

        // Wait for initial render and hydration (eval time only appears after useEffect)
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('initial')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#parent-eval-time').text()
          expect(text).toMatch(/Parent Evaluated At: \d+/)
        })

        // Capture the parent evaluation timestamp
        const parentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()

        // Verify initial accept call count
        const initialCallCount = await browser
          .elementByCss('#accept-call-count')
          .text()
        expect(initialCallCount).toBe('Accept Calls: 0')

        // Patch the dependency to change its exported value
        await next.patchFile('app/dep-accept/dep.ts', (content) =>
          content.replace("'initial'", "'updated'")
        )

        // Wait for the accept callback to fire and update the UI
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('updated')
        })

        // The accept callback should have been called
        await retry(async () => {
          const callCount = await browser
            .elementByCss('#accept-call-count')
            .text()
          expect(callCount).toBe('Accept Calls: 1')
        })

        // The parent module should NOT have been re-evaluated
        const newParentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()
        expect(newParentEvalTime).toBe(parentEvalTime)
      }
    )
  })

  describe('dependency accept with array', () => {
    itTurbopackDev(
      'parent accepts multiple child dependencies via array',
      async () => {
        const browser = await next.browser('/dep-accept-array')

        // Wait for initial render
        await retry(async () => {
          const text = await browser.elementByCss('#dep-a-value').text()
          expect(text).toBe('initial-a')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#dep-b-value').text()
          expect(text).toBe('initial-b')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#parent-eval-time').text()
          expect(text).toMatch(/Parent Evaluated At: \d+/)
        })

        const parentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()

        // Patch dep-a
        await next.patchFile('app/dep-accept-array/dep-a.ts', (content) =>
          content.replace("'initial-a'", "'updated-a'")
        )

        await retry(async () => {
          const text = await browser.elementByCss('#dep-a-value').text()
          expect(text).toBe('updated-a')
        })

        await retry(async () => {
          const callCount = await browser
            .elementByCss('#accept-call-count')
            .text()
          expect(callCount).toBe('Accept Calls: 1')
        })

        // Parent should NOT have been re-evaluated
        const evalTimeAfterA = await browser
          .elementByCss('#parent-eval-time')
          .text()
        expect(evalTimeAfterA).toBe(parentEvalTime)

        // Patch dep-b
        await next.patchFile('app/dep-accept-array/dep-b.ts', (content) =>
          content.replace("'initial-b'", "'updated-b'")
        )

        await retry(async () => {
          const text = await browser.elementByCss('#dep-b-value').text()
          expect(text).toBe('updated-b')
        })

        await retry(async () => {
          const callCount = await browser
            .elementByCss('#accept-call-count')
            .text()
          expect(callCount).toBe('Accept Calls: 2')
        })

        // Parent should still NOT have been re-evaluated
        const evalTimeAfterB = await browser
          .elementByCss('#parent-eval-time')
          .text()
        expect(evalTimeAfterB).toBe(parentEvalTime)
      }
    )
  })

  describe('dependency accept via module.hot (CJS)', () => {
    itTurbopackDev(
      'CJS module registers module.hot.accept and receives dep updates',
      async () => {
        const browser = await next.browser('/dep-accept-cjs')

        // Wait for initial render
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('initial')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#parent-eval-time').text()
          expect(text).toMatch(/Parent Evaluated At: \d+/)
        })

        const parentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()

        expect(await browser.elementByCss('#accept-call-count').text()).toBe(
          'Accept Calls: 0'
        )

        // Patch the dependency
        await next.patchFile('app/dep-accept-cjs/dep.cjs', (content) =>
          content.replace("'initial'", "'updated'")
        )

        // dep-observer.cjs handles the update via module.hot.accept
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('updated')
        })

        await retry(async () => {
          const callCount = await browser
            .elementByCss('#accept-call-count')
            .text()
          expect(callCount).toBe('Accept Calls: 1')
        })

        // page.tsx should NOT have been re-evaluated (no full reload)
        const newParentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()
        expect(newParentEvalTime).toBe(parentEvalTime)
      }
    )
  })

  describe('dependency decline', () => {
    itTurbopackDev(
      'declining a dependency triggers full reload on update',
      async () => {
        const browser = await next.browser('/dep-decline')

        // Wait for initial render
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('initial')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#parent-eval-time').text()
          expect(text).toMatch(/Parent Evaluated At: \d+/)
        })

        const parentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()

        // Patch the declined dependency
        await next.patchFile('app/dep-decline/dep.ts', (content) =>
          content.replace("'initial'", "'updated'")
        )

        // Since the dep is declined, the update should cause a full page reload.
        // After reload, the page re-evaluates with the new dep value.
        await retry(async () => {
          const text = await browser.elementByCss('#dep-value').text()
          expect(text).toBe('updated')
        })

        // The parent module SHOULD have been re-evaluated (full reload)
        await retry(async () => {
          const newParentEvalTime = await browser
            .elementByCss('#parent-eval-time')
            .text()
          expect(newParentEvalTime).not.toBe(parentEvalTime)
        })
      }
    )
  })

  describe('dependency decline with array', () => {
    itTurbopackDev(
      'declining multiple dependencies via array triggers full reload',
      async () => {
        const browser = await next.browser('/dep-decline-array')

        // Wait for initial render
        await retry(async () => {
          const text = await browser.elementByCss('#dep-a-value').text()
          expect(text).toBe('initial-a')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#dep-b-value').text()
          expect(text).toBe('initial-b')
        })
        await retry(async () => {
          const text = await browser.elementByCss('#parent-eval-time').text()
          expect(text).toMatch(/Parent Evaluated At: \d+/)
        })

        const parentEvalTime = await browser
          .elementByCss('#parent-eval-time')
          .text()

        // Patch dep-a (declined) — should trigger full reload
        await next.patchFile('app/dep-decline-array/dep-a.ts', (content) =>
          content.replace("'initial-a'", "'updated-a'")
        )

        await retry(async () => {
          const text = await browser.elementByCss('#dep-a-value').text()
          expect(text).toBe('updated-a')
        })

        // The parent module SHOULD have been re-evaluated (full reload)
        await retry(async () => {
          const newParentEvalTime = await browser
            .elementByCss('#parent-eval-time')
            .text()
          expect(newParentEvalTime).not.toBe(parentEvalTime)
        })
      }
    )
  })
})
Quest for Codev2.0.0
/
SIGN IN