You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Playwright中修改HTTP响应触发Service Worker更新

问题背景

开发单页应用(SPA)时接入了Service Worker能力:

  • 采用cache-first策略预缓存应用外壳
  • 采用network-first策略缓存导航请求

应用内置版本提示逻辑:当检测到处于waiting状态的Service Worker时,页面顶部会展示*A new version is available!*提示横幅,附带「Reload」按钮。在针对该提示逻辑和Reload按钮交互编写端到端测试时,始终无法找到在Playwright中触发Service Worker更新的有效方法。
初始实现思路为:Service Worker完成安装、触发页面重载后,修改Service Worker文件的HTTP响应。实际测试中可以正常修改响应体及响应头,但操作始终无法触发浏览器的Service Worker更新事件。

初始测试代码

test("shows an update button when the service worker file has changed", async ({
  page,
  context
}, testInfo) => {
  const newScriptBytes = "/*test*/"
  const timeout = testInfo.timeout
  await page.goto("/")
  await waitForReadyServiceWorker({ context, page, timeout })

  // change the bytes of all additional service worker requests
  await page.route("/service-worker.js", async route => {
    const serverResponse = await page.request.fetch(serviceWorkerUrl, {
      ignoreHTTPSErrors: true
    })
    let serviceWorkerScript = await serverResponse.text()
    serviceWorkerScript = serviceWorkerScript.concat(newScriptBytes)
    await route.fulfill({
      response: serverResponse,
      body: serviceWorkerScript,
      headers: {
        ...serverResponse.headers(),
        "Etag": "v1",
        "Last-Modified": new Date().toUTCString(),
        "Content-Length": serviceWorkerScript.length.toString()
      }
    })
  })

  await page.reload()

  await expect(
    page.locator(`text=A new version is available!`)
  ).toBeVisible()
})

export async function waitForReadyServiceWorker(args: {
  context: BrowserContext
  page: Page
  timeout?: number
}): Promise<Worker | undefined> {
  const timeoutId = createOperationTimeout(args.timeout)
  const url = await args.page.evaluate(async () => {
    const readySw = await navigator.serviceWorker.ready
    return readySw.active.scriptURL
  })
  clearTimeout(timeoutId)
  return args.context.serviceWorkers().find(worker => worker.url() === url)
}

相关截图

  • 初始Service Worker响应:
    initial service worker response
  • 修改后的Service Worker响应:
    modified service worker response

失败原因

原有方案无法触发更新核心有三个问题:

  • 拦截层级错误:page.route仅能拦截当前页面上下文发起的普通资源请求,Service Worker的更新检查是浏览器在BrowserContext层面独立发起的请求,不会经过page级路由拦截,因此修改SW响应的逻辑从未命中浏览器的SW更新请求。
  • 拦截注册时机过晚:等待首个SW完全激活后才注册路由规则,即使使用正确层级的拦截,也可能错过导航流程早期的SW更新检查时机。
  • 触发逻辑不可靠:依赖页面重载触发SW更新会受浏览器缓存规则影响,若透传的原始响应头携带强缓存字段(如长周期Cache-Control),浏览器会直接跳过SW更新请求,不会进入拦截逻辑。同时重载后未等待新SW进入waiting状态就直接断言,时序不匹配也会导致断言失败。

正确实现方案

按照以下步骤调整即可稳定复现SW更新场景:

  1. 替换拦截层级:使用context.route代替page.route,确保能拦截到浏览器层面发起的SW更新请求。
  2. 提前注册拦截规则:在页面跳转前就注册SW路由拦截,通过标志位控制返回原始/修改后的SW内容,避免错过请求时机。
  3. 强制跳过缓存:拦截SW响应时覆盖缓存相关头,返回Cache-Control: no-cache,避免浏览器走本地缓存跳过更新检查。
  4. 主动触发更新:通过页面JS调用registration.update()主动触发SW更新检查,同时监听SW的updatefound和statechange事件,等待新SW确实进入waiting状态后再做断言,避免时序问题。

修正后的测试代码如下:

import { test, expect, BrowserContext, Page, Worker } from "@playwright/test";

test("shows an update button when the service worker file has changed", async ({
  page,
  context
}, testInfo) => {
  let returnModifiedSw = false;
  const newScriptBytes = "/*test*/";
  const timeout = testInfo.timeout;

  // 提前在context层面注册SW拦截规则
  await context.route("/service-worker.js", async route => {
    const serverResponse = await route.fetch();
    let swScript = await serverResponse.text();
    
    if (returnModifiedSw) {
      swScript = swScript.concat(newScriptBytes);
    }

    await route.fulfill({
      response: serverResponse,
      body: swScript,
      headers: {
        ...serverResponse.headers(),
        // 强制无缓存,确保浏览器发起更新请求
        "Cache-Control": "no-cache",
        "Etag": returnModifiedSw ? "v2" : "v1",
        "Last-Modified": new Date().toUTCString(),
        "Content-Length": swScript.length.toString()
      }
    })
  })

  await page.goto("/")
  await waitForReadyServiceWorker({ context, page, timeout })

  // 切换为返回修改后的SW内容
  returnModifiedSw = true;

  // 主动触发SW更新,等待新SW进入waiting状态
  await page.evaluate(async () => {
    const registration = await navigator.serviceWorker.getRegistration();
    if (!registration) throw new Error("Service Worker registration not found");

    return new Promise<void>((resolve) => {
      // 新SW已经在waiting状态直接返回
      if (registration.waiting) {
        resolve();
        return;
      }

      // 监听新SW安装流程
      registration.addEventListener("updatefound", () => {
        const newWorker = registration.installing;
        if (!newWorker) return;
        newWorker.addEventListener("statechange", () => {
          if (newWorker.state === "installed") {
            resolve();
          }
        })
      })

      // 主动触发更新检查
      registration.update();
    })
  })

  // 断言更新提示可见
  await expect(
    page.locator(`text=A new version is available!`)
  ).toBeVisible()
})

async function waitForReadyServiceWorker(args: {
  context: BrowserContext
  page: Page
  timeout?: number
}): Promise<Worker | undefined> {
  const timeoutId = setTimeout(() => {
    throw new Error("Wait for Service Worker ready timeout")
  }, args.timeout ?? 30000)

  const swUrl = await args.page.evaluate(async () => {
    const readySw = await navigator.serviceWorker.ready
    return readySw.active?.scriptURL
  })
  clearTimeout(timeoutId)
  return args.context.serviceWorkers().find(worker => worker.url() === swUrl)
}

额外注意事项

  • 如果Service Worker中通过importScripts引入了其他拆分脚本,需要同步拦截这些脚本并修改内容,浏览器判断SW是否更新时会校验SW文件及其所有依赖脚本的总字节哈希,任意一个资源字节变化才会触发更新流程。
  • 测试Reload按钮激活新SW的逻辑时,点击按钮后可通过context.waitForEvent("serviceworker")监听新SW激活事件,确认新SW完全接管页面后再做后续断言即可。

内容的提问来源于stack exchange,提问作者Ethan Lemke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 05:12:23