Repository: vercel/next.js
Pull request: test: Isolate read-only-source-hmr test

This was flakey with Turbopack:
```
Error: TIMED OUT: /New page/

404
This page could not be found.

undefined
    at check (/root/actions-runner/_work/next.js/next.js/test/lib/next-test-utils.ts:726:9)
    at Object.<anonymous> (/root/actions-runner/_work/next.js/next.js/test/integration/read-only-source-hmr/test/index.test.ts:124:7)
```

But somehow Webpack doesn't automatically refresh the page here?


https://github.com/user-attachments/assets/b363dbff-60d1-4813-bb1c-e5a64f0312d3



Changed files:
filename: test/development/read-only-source-hmr/test/index.test.ts
patch: @@ -0,0 +1,156 @@
+import { nextTestSetup } from 'e2e-utils'
+import { getBrowserBodyText, retry } from 'next-test-utils'
+import fs from 'fs-extra'
+import path from 'path'
+import { type Playwright } from 'next-webdriver'
+
+const READ_ONLY_PERMISSIONS = 0o444
+const READ_WRITE_PERMISSIONS = 0o644
+
+let pageHello = 'pages/hello.js'
+
+describe('Read-only source HMR', () => {
+  const { next } = nextTestSetup({
+    files: path.join(__dirname, '..'),
+    skipStart: true,
+    env: {
+      __NEXT_TEST_WITH_DEVTOOL: '1',
+      // Events can be finicky in CI. This switches to a more reliable
+      // polling method.
+      CHOKIDAR_USEPOLLING: 'true',
+      CHOKIDAR_INTERVAL: '500',
+    },
+  })
+
+  beforeAll(async () => {
+    await fs.chmod(path.join(next.testDir, pageHello), READ_ONLY_PERMISSIONS)
+    await next.start()
+  })
+
+  async function patchFileReadOnly(
+    filename: string,
+    content: (content: string | undefined) => string | undefined,
+    runWithTempContent: (context: { newFile: boolean }) => Promise<void>
+  ) {
+    const filePath = path.join(next.testDir, filename)
+    const exists = await fs
+      .access(filePath)
+      .then(() => true)
+      .catch(() => false)
+
+    if (exists) {
+      await fs.chmod(filePath, READ_WRITE_PERMISSIONS)
+    }
+
+    const previousContent = exists ? await next.readFile(filename) : undefined
+
+    const newContent = content(previousContent)
+    if (newContent === undefined) {
+      if (exists) {
+        await fs.remove(filePath)
+      }
+    } else {
+      await fs.writeFile(filePath, newContent)
+    }
+
+    try {
+      if (newContent !== undefined) {
+        await fs.chmod(filePath, READ_ONLY_PERMISSIONS)
+      }
+      await runWithTempContent({ newFile: !exists })
+    } finally {
+      if (newContent !== undefined) {
+        await fs.chmod(filePath, READ_WRITE_PERMISSIONS)
+      }
+      if (previousContent === undefined) {
+        await fs.remove(filePath)
+      } else {
+        await fs.writeFile(filePath, previousContent)
+        await fs.chmod(filePath, READ_ONLY_PERMISSIONS)
+      }
+    }
+  }
+
+  it('should detect changes to a page', async () => {
+    let browser: Playwright
+
+    try {
+      browser = await next.browser('/hello')
+      await retry(async () =>
+        expect(await getBrowserBodyText(browser)).toContain('Hello World')
+      )
+
+      await patchFileReadOnly(
+        pageHello,
+        (content) => content.replace('Hello World', 'COOL page'),
+        async () => {
+          await retry(async () =>
+            expect(await getBrowserBodyText(browser)).toContain('COOL page')
+          )
+        }
+      )
+
+      await retry(async () =>
+        expect(await getBrowserBodyText(browser)).toContain('Hello World')
+      )
+    } finally {
+      await browser?.close()
+    }
+  })
+
+  it('should handle page deletion and subsequent recreation', async () => {
+    let browser: Playwright
+
+    try {
+      browser = await next.browser('/hello')
+      await retry(async () =>
+        expect(await getBrowserBodyText(browser)).toContain('Hello World')
+      )
+
+      await patchFileReadOnly(
+        pageHello,
+        () => undefined,
+        async () => {
+          await retry(async () =>
+            expect(await getBrowserBodyText(browser)).toContain(
+              'This page could not be found'
+            )
+          )
+        }
+      )
+
+      await retry(async () => {
+        if (!process.env.IS_TURBOPACK_TEST) {
+          // webpack doesn't automatically refresh the page when a page is added?
+          await browser.refresh()
+        }
+        expect(await getBrowserBodyText(browser)).toContain('Hello World')
+      })
+    } finally {
+      await browser?.close()
+    }
+  })
+
+  it('should detect a new page', async () => {
+    let browser: Playwright
+
+    try {
+      await patchFileReadOnly(
+        'pages/new.js',
+        () => `
+        const New = () => <p>New page</p>
+
+        export default New
+      `,
+        async () => {
+          browser = await next.browser('/new')
+          await retry(async () =>
+            expect(await getBrowserBodyText(browser)).toContain('New page')
+          )
+        }
+      )
+    } finally {
+      await browser?.close()
+    }
+  })
+})

filename: test/integration/read-only-source-hmr/test/index.test.ts
patch: @@ -1,132 +0,0 @@
-/* eslint-env jest */
-
-import fs from 'fs-extra'
-import {
-  check,
-  findPort,
-  getBrowserBodyText,
-  killApp,
-  launchApp,
-} from 'next-test-utils'
-import webdriver from 'next-webdriver'
-import { join } from 'path'
-
-const READ_ONLY_PERMISSIONS = 0o444
-const READ_WRITE_PERMISSIONS = 0o644
-
-const appDir = join(__dirname, '..')
-const pagePath = join(appDir, 'pages/hello.js')
-
-let appPort
-let app
-
-async function writeReadOnlyFile(path, content) {
-  const exists = await fs
-    .access(path)
-    .then(() => true)
-    .catch(() => false)
-
-  if (exists) {
-    await fs.chmod(path, READ_WRITE_PERMISSIONS)
-  }
-
-  await fs.writeFile(path, content, 'utf8')
-  await fs.chmod(path, READ_ONLY_PERMISSIONS)
-}
-
-describe('Read-only source HMR', () => {
-  beforeAll(async () => {
-    await fs.chmod(pagePath, READ_ONLY_PERMISSIONS)
-
-    appPort = await findPort()
-    app = await launchApp(appDir, appPort, {
-      env: {
-        __NEXT_TEST_WITH_DEVTOOL: '1',
-        // Events can be finicky in CI. This switches to a more reliable
-        // polling method.
-        CHOKIDAR_USEPOLLING: 'true',
-        CHOKIDAR_INTERVAL: '500',
-      },
-    })
-  })
-
-  afterAll(async () => {
-    await fs.chmod(pagePath, READ_WRITE_PERMISSIONS)
-    await killApp(app)
-  })
-
-  it('should detect changes to a page', async () => {
-    let browser
-
-    try {
-      browser = await webdriver(appPort, '/hello')
-      await check(() => getBrowserBodyText(browser), /Hello World/)
-
-      const originalContent = await fs.readFile(pagePath, 'utf8')
-      const editedContent = originalContent.replace('Hello World', 'COOL page')
-
-      if (process.env.IS_TURBOPACK_TEST) {
-        // TODO Turbopack needs a bit to start watching
-        await new Promise((resolve) => setTimeout(resolve, 500))
-      }
-
-      await writeReadOnlyFile(pagePath, editedContent)
-      await check(() => getBrowserBodyText(browser), /COOL page/)
-
-      await writeReadOnlyFile(pagePath, originalContent)
-      await check(() => getBrowserBodyText(browser), /Hello World/)
-    } finally {
-      if (browser) {
-        await browser.close()
-      }
-    }
-  })
-
-  it('should handle page deletion and subsequent recreation', async () => {
-    let browser
-
-    try {
-      browser = await webdriver(appPort, '/hello')
-      await check(() => getBrowserBodyText(browser), /Hello World/)
-
-      const originalContent = await fs.readFile(pagePath, 'utf8')
-
-      if (process.env.IS_TURBOPACK_TEST) {
-        // TODO Turbopack needs a bit to start watching
-        await new Promise((resolve) => setTimeout(resolve, 500))
-      }
-
-      await fs.remove(pagePath)
-      await writeReadOnlyFile(pagePath, originalContent)
-      await check(() => getBrowserBodyText(browser), /Hello World/)
-    } finally {
-      if (browser) {
-        await browser.close()
-      }
-    }
-  })
-
-  it('should detect a new page', async () => {
-    let browser
-    const newPagePath = join(appDir, 'pages/new.js')
-
-    try {
-      await writeReadOnlyFile(
-        newPagePath,
-        `
-        const New = () => <p>New page</p>
-
-        export default New
-      `
-      )
-
-      browser = await webdriver(appPort, '/new')
-      await check(() => getBrowserBodyText(browser), /New page/)
-    } finally {
-      if (browser) {
-        await browser.close()
-      }
-      await fs.remove(newPagePath)
-    }
-  })
-})