import { join } from 'path'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
import {
  check,
  getBrowserBodyText,
  getRedboxHeader,
  getRedboxSource,
  hasRedbox,
  renderViaHTTP,
  waitFor,
} from 'next-test-utils'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'

describe.each([[''], ['/docs']])(
  'basic HMR, basePath: %p',
  (basePath: string) => {
    let next: NextInstance

    beforeAll(async () => {
      next = await createNext({
        files: {
          pages: new FileRef(join(__dirname, 'hmr/pages')),
          components: new FileRef(join(__dirname, 'hmr/components')),
        },
        nextConfig: {
          basePath,
        },
      })
    })
    afterAll(() => next.destroy())

    it('should show hydration error correctly', async () => {
      const browser = await webdriver(next.url, basePath + '/hydration-error')
      await check(async () => {
        const logs = await browser.log()
        return logs.some((log) =>
          log.message.includes('messages/react-hydration-error')
        )
          ? 'success'
          : JSON.stringify(logs, null, 2)
      }, 'success')
    })

    it('should have correct router.isReady for auto-export page', async () => {
      let browser = await webdriver(
        next.url,
        basePath + '/auto-export-is-ready'
      )

      expect(await browser.elementByCss('#ready').text()).toBe('yes')
      expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual(
        {}
      )

      browser = await webdriver(
        next.url,
        basePath + '/auto-export-is-ready?hello=world'
      )

      await check(async () => {
        return browser.elementByCss('#ready').text()
      }, 'yes')
      expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
        hello: 'world',
      })
    })

    it('should have correct router.isReady for getStaticProps page', async () => {
      let browser = await webdriver(next.url, basePath + '/gsp-is-ready')

      expect(await browser.elementByCss('#ready').text()).toBe('yes')
      expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual(
        {}
      )

      browser = await webdriver(
        next.url,
        basePath + '/gsp-is-ready?hello=world'
      )

      await check(async () => {
        return browser.elementByCss('#ready').text()
      }, 'yes')
      expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
        hello: 'world',
      })
    })

    describe('Hot Module Reloading', () => {
      describe('delete a page and add it back', () => {
        it('should load the page properly', async () => {
          const contactPagePath = join('pages', 'hmr', 'contact.js')
          const newContactPagePath = join('pages', 'hmr', '_contact.js')
          let browser
          try {
            const start = next.cliOutput.length
            browser = await webdriver(next.url, basePath + '/hmr/contact')
            const text = await browser.elementByCss('p').text()
            expect(text).toBe('This is the contact page.')

            // Rename the file to mimic a deleted page
            await next.renameFile(contactPagePath, newContactPagePath)

            await check(
              () => getBrowserBodyText(browser),
              /This page could not be found/
            )

            // Rename the file back to the original filename
            await next.renameFile(newContactPagePath, contactPagePath)

            // wait until the page comes back
            await check(
              () => getBrowserBodyText(browser),
              /This is the contact page/
            )

            expect(next.cliOutput.slice(start)).toContain('compiling...')
            expect(next.cliOutput.slice(start)).toContain(
              'compiling /hmr/contact (client and server)...'
            )
            expect(next.cliOutput).toContain(
              'compiling /_error (client and server)...'
            )
          } finally {
            if (browser) {
              await browser.close()
            }
            await next
              .renameFile(newContactPagePath, contactPagePath)
              .catch(() => {})
          }
        })
      })

      describe('editing a page', () => {
        it('should detect the changes and display it', async () => {
          let browser
          try {
            browser = await webdriver(next.url, basePath + '/hmr/about')
            const text = await browser.elementByCss('p').text()
            expect(text).toBe('This is the about page.')

            const aboutPagePath = join('pages', 'hmr', 'about.js')

            const originalContent = await next.readFile(aboutPagePath)
            const editedContent = originalContent.replace(
              'This is the about page',
              'COOL page'
            )

            // change the content
            try {
              await next.patchFile(aboutPagePath, editedContent)
              await check(() => getBrowserBodyText(browser), /COOL page/)
            } finally {
              // add the original content
              await next.patchFile(aboutPagePath, originalContent)
            }

            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          } finally {
            if (browser) {
              await browser.close()
            }
          }
        })

        it('should not reload unrelated pages', async () => {
          let browser
          try {
            browser = await webdriver(next.url, basePath + '/hmr/counter')
            const text = await browser
              .elementByCss('button')
              .click()
              .elementByCss('button')
              .click()
              .elementByCss('p')
              .text()
            expect(text).toBe('COUNT: 2')

            const aboutPagePath = join('pages', 'hmr', 'about.js')

            const originalContent = await next.readFile(aboutPagePath)
            const editedContent = originalContent.replace(
              'This is the about page',
              'COOL page'
            )

            try {
              // Change the about.js page
              await next.patchFile(aboutPagePath, editedContent)

              // Check whether the this page has reloaded or not.
              await check(() => browser.elementByCss('p').text(), /COUNT: 2/)
            } finally {
              // restore the about page content.
              await next.patchFile(aboutPagePath, originalContent)
            }
          } finally {
            if (browser) {
              await browser.close()
            }
          }
        })

        // Added because of a regression in react-hot-loader, see issues: #4246 #4273
        // Also: https://github.com/vercel/styled-jsx/issues/425
        it('should update styles correctly', async () => {
          let browser
          try {
            browser = await webdriver(next.url, basePath + '/hmr/style')
            const pTag = await browser.elementByCss('.hmr-style-page p')
            const initialFontSize = await pTag.getComputedCss('font-size')

            expect(initialFontSize).toBe('100px')

            const pagePath = join('pages', 'hmr', 'style.js')

            const originalContent = await next.readFile(pagePath)
            const editedContent = originalContent.replace('100px', '200px')

            // Change the page
            await next.patchFile(pagePath, editedContent)

            try {
              // Check whether the this page has reloaded or not.
              await check(async () => {
                const editedPTag = await browser.elementByCss(
                  '.hmr-style-page p'
                )
                return editedPTag.getComputedCss('font-size')
              }, /200px/)
            } finally {
              // Finally is used so that we revert the content back to the original regardless of the test outcome
              // restore the about page content.
              await next.patchFile(pagePath, originalContent)
            }
          } finally {
            if (browser) {
              await browser.close()
            }
          }
        })

        // Added because of a regression in react-hot-loader, see issues: #4246 #4273
        // Also: https://github.com/vercel/styled-jsx/issues/425
        it('should update styles in a stateful component correctly', async () => {
          let browser
          const pagePath = join('pages', 'hmr', 'style-stateful-component.js')
          const originalContent = await next.readFile(pagePath)
          try {
            browser = await webdriver(
              next.url,
              basePath + '/hmr/style-stateful-component'
            )
            const pTag = await browser.elementByCss('.hmr-style-page p')
            const initialFontSize = await pTag.getComputedCss('font-size')

            expect(initialFontSize).toBe('100px')
            const editedContent = originalContent.replace('100px', '200px')

            // Change the page
            await next.patchFile(pagePath, editedContent)

            // Check whether the this page has reloaded or not.
            await check(async () => {
              const editedPTag = await browser.elementByCss('.hmr-style-page p')
              return editedPTag.getComputedCss('font-size')
            }, /200px/)
          } finally {
            if (browser) {
              await browser.close()
            }
            await next.patchFile(pagePath, originalContent)
          }
        })

        // Added because of a regression in react-hot-loader, see issues: #4246 #4273
        // Also: https://github.com/vercel/styled-jsx/issues/425
        it('should update styles in a dynamic component correctly', async () => {
          let browser = null
          let secondBrowser = null
          const pagePath = join('components', 'hmr', 'dynamic.js')
          const originalContent = await next.readFile(pagePath)
          try {
            browser = await webdriver(
              next.url,
              basePath + '/hmr/style-dynamic-component'
            )
            const div = await browser.elementByCss('#dynamic-component')
            const initialClientClassName = await div.getAttribute('class')
            const initialFontSize = await div.getComputedCss('font-size')

            expect(initialFontSize).toBe('100px')

            const initialHtml = await renderViaHTTP(
              next.url,
              basePath + '/hmr/style-dynamic-component'
            )
            expect(initialHtml.includes('100px')).toBeTruthy()

            const $initialHtml = cheerio.load(initialHtml)
            const initialServerClassName =
              $initialHtml('#dynamic-component').attr('class')

            expect(
              initialClientClassName === initialServerClassName
            ).toBeTruthy()

            const editedContent = originalContent.replace('100px', '200px')

            // Change the page
            await next.patchFile(pagePath, editedContent)

            // wait for 5 seconds
            await waitFor(5000)

            secondBrowser = await webdriver(
              next.url,
              basePath + '/hmr/style-dynamic-component'
            )
            // Check whether the this page has reloaded or not.
            const editedDiv = await secondBrowser.elementByCss(
              '#dynamic-component'
            )
            const editedClientClassName = await editedDiv.getAttribute('class')
            const editedFontSize = await editedDiv.getComputedCss('font-size')
            const browserHtml = await secondBrowser.eval(
              'document.documentElement.innerHTML'
            )

            expect(editedFontSize).toBe('200px')
            expect(browserHtml.includes('font-size:200px')).toBe(true)
            expect(browserHtml.includes('font-size:100px')).toBe(false)

            const editedHtml = await renderViaHTTP(
              next.url,
              basePath + '/hmr/style-dynamic-component'
            )
            expect(editedHtml.includes('200px')).toBeTruthy()
            const $editedHtml = cheerio.load(editedHtml)
            const editedServerClassName =
              $editedHtml('#dynamic-component').attr('class')

            expect(editedClientClassName === editedServerClassName).toBe(true)
          } finally {
            // Finally is used so that we revert the content back to the original regardless of the test outcome
            // restore the about page content.
            await next.patchFile(pagePath, originalContent)

            if (browser) {
              await browser.close()
            }

            if (secondBrowser) {
              secondBrowser.close()
            }
          }
        })
      })
    })

    describe('Error Recovery', () => {
      it('should recover from 404 after a page has been added', async () => {
        let browser
        const newPage = join('pages', 'hmr', 'new-page.js')

        try {
          const start = next.cliOutput.length
          browser = await webdriver(next.url, basePath + '/hmr/new-page')

          expect(await browser.elementByCss('body').text()).toMatch(
            /This page could not be found/
          )

          // Add the page
          await next.patchFile(
            newPage,
            'export default () => (<div id="new-page">the-new-page</div>)'
          )

          await check(() => getBrowserBodyText(browser), /the-new-page/)

          await next.deleteFile(newPage)

          await check(
            () => getBrowserBodyText(browser),
            /This page could not be found/
          )

          expect(next.cliOutput.slice(start)).toContain(
            'compiling /hmr/new-page (client and server)...'
          )
          expect(next.cliOutput).toContain(
            'compiling /_error (client and server)...'
          )
        } catch (err) {
          await next.deleteFile(newPage)
          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should detect syntax errors and recover', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about2.js')
        const aboutContent = await next.readFile(aboutPage)
        try {
          const start = next.cliOutput.length
          browser = await webdriver(next.url, basePath + '/hmr/about2')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(aboutPage, aboutContent.replace('</div>', 'div'))

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
          expect(next.cliOutput.slice(start)).toContain(
            'compiling /hmr/about2 (client and server)...'
          )
          expect(next.cliOutput).toContain(
            'compiling /_error (client and server)...'
          )
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)
          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should show the error on all pages', async () => {
        const aboutPage = join('pages', 'hmr', 'about2.js')
        const aboutContent = await next.readFile(aboutPage)
        let browser
        try {
          await renderViaHTTP(next.url, basePath + '/hmr/about2')

          await next.patchFile(aboutPage, aboutContent.replace('</div>', 'div'))

          // Ensure dev server has time to break:
          await new Promise((resolve) => setTimeout(resolve, 2000))

          browser = await webdriver(next.url, basePath + '/hmr/contact')

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the contact page/
          )
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)
          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the contact page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should detect runtime errors on the module scope', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about3.js')
        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/about3')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace('export', 'aa=20;\nexport')
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
        } finally {
          await next.patchFile(aboutPage, aboutContent)
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover from errors in the render function', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about4.js')
        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/about4')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'return',
              'throw new Error("an-expected-error");\nreturn'
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxSource(browser)).toMatch(/an-expected-error/)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)
          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after exporting an invalid page', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about5.js')
        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/about5')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'export default',
              'export default {};\nexport const fn ='
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
                      " 1 of 1 unhandled error
                      Server Error

                      Error: The default export is not a React Component in page: \\"/hmr/about5\\"

                      This error happened while generating the page. Any console logs will be displayed in the terminal window."
                  `)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)

          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after a bad return from the render function', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about6.js')
        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/about6')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'export default',
              'export default () => /search/;\nexport const fn ='
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          // TODO: Replace this when webpack 5 is the default
          expect(await getRedboxHeader(browser)).toMatch(
            `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.`
          )

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)

          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after undefined exported as default', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about7.js')

        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/about7')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'export default',
              'export default undefined;\nexport const fn ='
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
                      " 1 of 1 unhandled error
                      Server Error

                      Error: The default export is not a React Component in page: \\"/hmr/about7\\"

                      This error happened while generating the page. Any console logs will be displayed in the terminal window."
                  `)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
          expect(await hasRedbox(browser, false)).toBe(false)
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)

          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after webpack parse error in an imported file', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about8.js')

        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.appPort, basePath + '/hmr/about8')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'export default',
              'import "../../components/parse-error.xyz"\nexport default'
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(
            `"Failed to compile"`
          )
          expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
                      "./components/parse-error.xyz
                      Module parse failed: Unexpected token (3:0)
                      You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
                      | This
                      | is
                      > }}}
                      | invalid
                      | js

                      Import trace for requested module:
                      ./components/parse-error.xyz
                      ./pages/hmr/about8.js"
                  `)

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
          expect(await hasRedbox(browser, false)).toBe(false)
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)

          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after loader parse error in an imported file', async () => {
        let browser
        const aboutPage = join('pages', 'hmr', 'about9.js')

        const aboutContent = await next.readFile(aboutPage)
        try {
          browser = await webdriver(next.appPort, basePath + '/hmr/about9')
          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )

          await next.patchFile(
            aboutPage,
            aboutContent.replace(
              'export default',
              'import "../../components/parse-error.js"\nexport default'
            )
          )

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(
            `"Failed to compile"`
          )
          let redboxSource = await getRedboxSource(browser)

          redboxSource = redboxSource.replace(`${next.testDir}`, '.')
          redboxSource = redboxSource.substring(
            0,
            redboxSource.indexOf('`----')
          )

          expect(
            next.normalizeTestDirContent(redboxSource)
          ).toMatchInlineSnapshot(
            next.normalizeSnapshot(`
              "./components/parse-error.js
              Error: 
                x Expression expected
                 ,-[./components/parse-error.js:1:1]
               1 | This
               2 | is
               3 | }}}
                 : ^
               4 | invalid
               5 | js
                 "
            `)
          )

          await next.patchFile(aboutPage, aboutContent)

          await check(
            () => getBrowserBodyText(browser),
            /This is the about page/
          )
          expect(await hasRedbox(browser, false)).toBe(false)
        } catch (err) {
          await next.patchFile(aboutPage, aboutContent)

          if (browser) {
            await check(
              () => getBrowserBodyText(browser),
              /This is the about page/
            )
          }

          if (!process.env.NEXT_SWC_DEV_BIN) {
            throw err
          }
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover from errors in getInitialProps in client', async () => {
        let browser
        const erroredPage = join('pages', 'hmr', 'error-in-gip.js')
        const errorContent = await next.readFile(erroredPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr')
          await browser.elementByCss('#error-in-gip-link').click()

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
            " 1 of 1 unhandled error
            Unhandled Runtime Error

            Error: an-expected-error-in-gip"
          `)

          await next.patchFile(
            erroredPage,
            errorContent.replace('throw error', 'return {}')
          )

          await check(() => getBrowserBodyText(browser), /Hello/)

          await next.patchFile(erroredPage, errorContent)

          await check(async () => {
            await browser.refresh()
            await waitFor(2000)
            const text = await getBrowserBodyText(browser)
            if (text.includes('Hello')) {
              throw new Error('waiting')
            }
            return getRedboxSource(browser)
          }, /an-expected-error-in-gip/)
        } catch (err) {
          await next.patchFile(erroredPage, errorContent)

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })

      it('should recover after an error reported via SSR', async () => {
        let browser
        const erroredPage = join('pages', 'hmr', 'error-in-gip.js')
        const errorContent = await next.readFile(erroredPage)
        try {
          browser = await webdriver(next.url, basePath + '/hmr/error-in-gip')

          expect(await hasRedbox(browser, true)).toBe(true)
          expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
                      " 1 of 1 unhandled error
                      Server Error

                      Error: an-expected-error-in-gip

                      This error happened while generating the page. Any console logs will be displayed in the terminal window."
                  `)

          const erroredPage = join('pages', 'hmr', 'error-in-gip.js')

          await next.patchFile(
            erroredPage,
            errorContent.replace('throw error', 'return {}')
          )

          await check(() => getBrowserBodyText(browser), /Hello/)

          await next.patchFile(erroredPage, errorContent)

          await check(async () => {
            await browser.refresh()
            await waitFor(2000)
            const text = await getBrowserBodyText(browser)
            if (text.includes('Hello')) {
              throw new Error('waiting')
            }
            return getRedboxSource(browser)
          }, /an-expected-error-in-gip/)
        } catch (err) {
          await next.patchFile(erroredPage, errorContent)

          throw err
        } finally {
          if (browser) {
            await browser.close()
          }
        }
      })
    })

    describe('Full reload', () => {
      it('should warn about full reload in cli output - anonymous page function', async () => {
        const start = next.cliOutput.length
        const browser = await webdriver(
          next.url,
          basePath + '/hmr/anonymous-page-function'
        )
        const cliWarning =
          'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload'

        expect(await browser.elementByCss('p').text()).toBe('hello world')
        expect(next.cliOutput.slice(start)).not.toContain(cliWarning)

        const currentFileContent = await next.readFile(
          './pages/hmr/anonymous-page-function.js'
        )
        const newFileContent = currentFileContent.replace(
          '<p>hello world</p>',
          '<p id="updated">hello world!!!</p>'
        )
        await next.patchFile(
          './pages/hmr/anonymous-page-function.js',
          newFileContent
        )

        expect(await browser.waitForElementByCss('#updated').text()).toBe(
          'hello world!!!'
        )

        // CLI warning
        expect(next.cliOutput.slice(start)).toContain(cliWarning)

        // Browser warning
        const browserLogs = await browser.log()
        expect(
          browserLogs.some(({ message }) =>
            message.includes(
              "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree."
            )
          )
        ).toBeTruthy()
      })

      it('should warn about full reload in cli output - runtime-error', async () => {
        const start = next.cliOutput.length
        const browser = await webdriver(
          next.url,
          basePath + '/hmr/runtime-error'
        )
        const cliWarning =
          'Fast Refresh had to perform a full reload due to a runtime error.'

        await check(
          () => getRedboxHeader(browser),
          /ReferenceError: whoops is not defined/
        )
        expect(next.cliOutput.slice(start)).not.toContain(cliWarning)

        const currentFileContent = await next.readFile(
          './pages/hmr/runtime-error.js'
        )
        const newFileContent = currentFileContent.replace(
          'whoops',
          '<p id="updated">whoops</p>'
        )
        await next.patchFile('./pages/hmr/runtime-error.js', newFileContent)

        expect(await browser.waitForElementByCss('#updated').text()).toBe(
          'whoops'
        )

        // CLI warning
        expect(next.cliOutput.slice(start)).toContain(cliWarning)

        // Browser warning
        const browserLogs = await browser.log()
        expect(
          browserLogs.some(({ message }) =>
            message.includes(
              '[Fast Refresh] performing full reload because your application had an unrecoverable error'
            )
          )
        ).toBeTruthy()
      })
    })

    it('should have client HMR events in trace file', async () => {
      const traceData = await next.readFile('.next/trace')
      expect(traceData).toContain('client-hmr-latency')
      expect(traceData).toContain('client-error')
      expect(traceData).toContain('client-success')
      expect(traceData).toContain('client-full-reload')
    })

    it('should have correct compile timing after fixing error', async () => {
      const pageName = 'pages/auto-export-is-ready.js'
      const originalContent = await next.readFile(pageName)

      try {
        const browser = await webdriver(
          next.url,
          basePath + '/auto-export-is-ready'
        )
        const outputLength = next.cliOutput.length
        await next.patchFile(
          pageName,
          `import hello from 'non-existent'\n` + originalContent
        )
        expect(await hasRedbox(browser, true)).toBe(true)
        await waitFor(3000)
        await next.patchFile(pageName, originalContent)
        await check(
          () => next.cliOutput.substring(outputLength),
          /compiled.*?successfully/i
        )
        const compileTime = next.cliOutput
          .substring(outputLength)
          .match(/compiled.*?successfully in ([\d.]{1,})\s?(?:s|ms)/i)

        let compileTimeMs = parseFloat(compileTime[1])
        if (
          next.cliOutput
            .substring(outputLength)
            .match(/compiled.*?successfully in ([\d.]{1,})\s?s/)
        ) {
          compileTimeMs = compileTimeMs * 1000
        }
        expect(compileTimeMs).toBeLessThan(3000)
      } finally {
        await next.patchFile(pageName, originalContent)
      }
    })
  }
)
