diff --git a/e2e/popup-autosize.spec.ts b/e2e/popup-autosize.spec.ts
new file mode 100644
index 000000000..5f5c02f7c
--- /dev/null
+++ b/e2e/popup-autosize.spec.ts
@@ -0,0 +1,213 @@
+import fs from "fs";
+import os from "os";
+import path from "path";
+import { chromium, type BrowserContext, type CDPSession, type Worker } from "@playwright/test";
+import { test, expect } from "./fixtures";
+import { headlessArgs } from "./launch-args";
+
+const pathToExtension = path.resolve(__dirname, "../dist/ext");
+
+// Chrome 默认缩放存成以 1.2 为底的对数级别;扩展 popup 跟随默认缩放。
+function zoomLevel(factor: number) {
+ return Math.log(factor) / Math.log(1.2);
+}
+
+async function launchWithExtension(options: { zoom?: number; mobile?: boolean } = {}) {
+ const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pw-popup-autosize-"));
+ if (options.zoom) {
+ fs.mkdirSync(path.join(userDataDir, "Default"), { recursive: true });
+ fs.writeFileSync(
+ path.join(userDataDir, "Default", "Preferences"),
+ JSON.stringify({ partition: { default_zoom_level: { x: zoomLevel(options.zoom) } } })
+ );
+ }
+ const context = await chromium.launchPersistentContext(userDataDir, {
+ headless: false,
+ args: [
+ ...headlessArgs(),
+ `--disable-extensions-except=${pathToExtension}`,
+ `--load-extension=${pathToExtension}`,
+ "--disable-gpu",
+ ],
+ ...(options.mobile
+ ? { viewport: { width: 412, height: 915 }, isMobile: true, hasTouch: true, deviceScaleFactor: 2.625 }
+ : {}),
+ timeout: 60_000,
+ });
+ await context.addInitScript(() => {
+ try {
+ localStorage.setItem("firstUse", "false");
+ } catch {
+ // about:blank 等不透明来源无法访问 localStorage
+ }
+ });
+ let [sw] = context.serviceWorkers();
+ if (!sw) sw = await context.waitForEvent("serviceworker", { timeout: 14_000 });
+ return {
+ context,
+ sw,
+ extensionId: sw.url().split("/")[2],
+ cleanup: async () => {
+ await context.close();
+ fs.rmSync(userDataDir, { recursive: true, force: true });
+ },
+ };
+}
+
+async function installMatchingScripts(context: BrowserContext, extensionId: string, count: number) {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/src/options.html`);
+ for (let i = 0; i < count; i++) {
+ const code = [
+ "// ==UserScript==",
+ `// @name Autosize probe ${i}`,
+ "// @namespace e2e-popup-autosize",
+ "// @version 1.0",
+ "// @match https://example.com/*",
+ "// ==/UserScript==",
+ "",
+ ].join("\n");
+ await page.evaluate(
+ (code) =>
+ new Promise((resolve) =>
+ chrome.runtime.sendMessage(
+ {
+ action: "serviceWorker/script/installByCode",
+ data: { uuid: crypto.randomUUID(), code, upsertBy: "user" },
+ },
+ resolve
+ )
+ ),
+ code
+ );
+ }
+ await page.close();
+}
+
+/**
+ * 以真实 action popup 打开(chrome.action.openPopup),而不是在标签页里加载 popup.html:
+ * 只有真正的 popup 窗口由浏览器按内容自动定宽定高,#1800 的塌缩只在这里出现。
+ * Playwright 不把 popup 暴露成 Page,只能经 CDP Target 域附着求值。
+ */
+async function openActionPopup(context: BrowserContext, sw: Worker) {
+ const tab = await context.newPage();
+ await tab.route("https://example.com/**", (route) =>
+ route.fulfill({ contentType: "text/html", body: "
popup autosize" })
+ );
+ await tab.goto("https://example.com/");
+ for (const p of context.pages()) if (p !== tab) await p.close();
+ await tab.bringToFront();
+ await sw.evaluate(async () => {
+ const [win] = await chrome.windows.getAll({ windowTypes: ["normal"] });
+ await chrome.action.openPopup({ windowId: win.id });
+ });
+
+ const cdp: CDPSession = await context.newCDPSession(tab);
+ let targetId: string | undefined;
+ await expect
+ .poll(async () => {
+ const { targetInfos } = await cdp.send("Target.getTargets");
+ targetId = targetInfos.find((t) => t.url.includes("/src/popup.html"))?.targetId;
+ return targetId;
+ })
+ .toBeTruthy();
+ const { sessionId } = await cdp.send("Target.attachToTarget", { targetId: targetId!, flatten: false });
+
+ let seq = 0;
+ return function evaluate(expression: string): Promise {
+ const id = ++seq;
+ return new Promise((resolve, reject) => {
+ const onMessage = (event: { sessionId: string; message: string }) => {
+ if (event.sessionId !== sessionId) return;
+ const message = JSON.parse(event.message);
+ if (message.id !== id) return;
+ cdp.off("Target.receivedMessageFromTarget", onMessage);
+ if (message.error || message.result?.exceptionDetails) {
+ reject(new Error(JSON.stringify(message.error ?? message.result.exceptionDetails)));
+ } else {
+ resolve(message.result.result.value as T);
+ }
+ };
+ cdp.on("Target.receivedMessageFromTarget", onMessage);
+ void cdp.send("Target.sendMessageToTarget", {
+ sessionId,
+ message: JSON.stringify({ id, method: "Runtime.evaluate", params: { expression, returnByValue: true } }),
+ });
+ });
+ };
+}
+
+type PopupLayout = {
+ contentWidth: number;
+ viewportHeight: number;
+ headerTop: number;
+ footerBottom: number;
+ innerScrollers: number;
+ documentScrollbarThemed: boolean;
+};
+
+// 页眉以 Header 组件的 为准,页脚为