CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/playwright-extension-fixtures

Author the Playwright fixture layer every Chromium extension test depends on - `chromium.launchPersistentContext` with `--disable-extensions-except=$DIR` + `--load-extension=$DIR`, the `channel: 'chromium'` selection that unlocks headless extension support, the `context.serviceWorkers()` + `waitForEvent('serviceworker')` race-handling pattern, and the `extensionId = serviceWorker.url().split('/')[2]` extraction recipe - plus, in references/, the extension load / reload matrix (which edit re-evaluates which surface), the `chrome.storage` test suite (area selection, quota-exceeded, `storage.onChanged`, managed read-only), and the per-surface assertion recipes (popup, content script, background messaging, MV3 auto-suspend survival). Use when authoring or debugging the fixture a Chromium extension test imports, when an edit appears to have no effect and you need the reload matrix, or when authoring popup / content-script / storage tests.

69

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

storage-tests.mdreferences/

chrome.storage test suite - area selection, quotas, events

Companion reference for playwright-extension-fixtures. Consult when an extension persists state and no test proves the chosen storage area survives its quota limits. Every template runs from this fixture's service-worker context (sw.evaluate(...)).

chrome.storage has four areas with non-overlapping quotas, persistence semantics, and enterprise-policy posture. Picking the wrong one is a class of bug the type checker can't catch: storage.sync silently rejects writes past its limits, storage.session evaporates on browser restart, and storage.managed is read-only and throws on write.

Area decision matrix

All constants per cr-storage:

AreaQUOTA_BYTESPer-itemLifetimeCross-deviceWriter
storage.local10,485,760 (10 MB)none documenteduntil extension removalnoextension
storage.sync102,400 (~100 KB)8,192 (8 KB)persistent, syncedyes (when user signed in)extension
storage.session10,485,760 (10 MB)none documentedcleared on disable, reload, update, or browser restart; MV3-onlynoextension
storage.managed--as long as policy is in effectvariesadmin only - read-only for the extension

storage.sync throughput caps: MAX_ITEMS = 512, MAX_WRITE_OPERATIONS_PER_MINUTE = 120, MAX_WRITE_OPERATIONS_PER_HOUR = 1,800. MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE is deprecated ("no longer has a sustained write operation quota" per cr-storage) - drop any assertion on it.

Version-specific floors - pin tests to the live constant, never a hard-coded number: storage.local was 5 MB in Chrome 113 and earlier; storage.session was 1 MB in Chrome 111 and earlier (per cr-storage).

Quota-exceeded template (per-item)

Quota-exceeded writes fail immediately and set runtime.lastError (callback form) or reject the Promise (async form); drive both paths:

test('storage.sync rejects on per-item quota exceeded', async ({ context }) => {
  let [sw] = context.serviceWorkers();
  if (!sw) sw = await context.waitForEvent('serviceworker');

  const result = await sw.evaluate(async () => {
    const oversized = 'x'.repeat(9 * 1024); // 9 KB > 8 KB limit
    try {
      await chrome.storage.sync.set({ big: oversized });
      return { ok: true };
    } catch (e: any) {
      return { ok: false, message: e.message };
    }
  });

  expect(result.ok).toBe(false);
  // exact message is unstable; assert quota-shaped text
  expect(result.message).toMatch(/quota|QUOTA_BYTES/i);
});

Callback-path equivalence:

const err = await sw.evaluate(() => new Promise<string | null>(resolve => {
  const oversized = 'x'.repeat(9 * 1024);
  chrome.storage.sync.set({ big: oversized }, () => {
    resolve(chrome.runtime.lastError?.message ?? null);
  });
}));
expect(err).toMatch(/quota|QUOTA_BYTES/i);

The remaining suite cells

TestAsserts
storage.sync total-quotaa batch write past QUOTA_BYTES = 102,400 rejects (e.g. 13 items x ~8 KB)
MAX_ITEMS (512)the 513th unique key rejects
storage.onChanged shapelistener receives (changes, areaName) with changes[key] = { oldValue?, newValue? } - same shape on Firefox per mdn-storage
multi-area isolationa storage.local write is invisible to storage.sync.get
storage.managed read-onlyany managed.set rejects ("Trying to modify this namespace results in an error" per mdn-storage)
session lifetimestorage.session data does not survive browser restart

storage.onChanged shape test:

const event = await sw.evaluate(() => new Promise<any>(resolve => {
  chrome.storage.onChanged.addListener(function listener(changes, area) {
    chrome.storage.onChanged.removeListener(listener);
    resolve({ changes, area });
  });
  chrome.storage.local.set({ theme: 'dark' });
}));
expect(event.area).toBe('local');
expect(event.changes.theme.newValue).toBe('dark');
expect(event.changes.theme.oldValue).toBeUndefined(); // first write

Firefox-Chrome divergences

ConcernChromeFirefox
storage.sync quotas102,400 / 8,192 / 512 / 1,800 per hour per cr-storageMDN does not enumerate quota numbers on the high-level page; align to the StorageArea sub-page values and verify on Firefox stable
storage.session MV3-onlyYes per cr-storageYes per mdn-storage - skip session tests when targeting MV2
storage.managedAvailable; admin-configured per OSAvailable per mdn-storage; policy injection mechanism differs per OS
Sync sign-inChrome account requiredFirefox account required - skip sync round-trip tests on machines without sign-in

Firefox's storage.local "persists even when users clear browsing history/data (unlike localStorage)" per mdn-storage - useful for a clear-history regression test.

Anti-patterns and limits

  • Happy-path-only set + get - quota silently drops past 8 KB / 100 KB; author the quota tests above.
  • Asserting exact error message strings - wording isn't pinned; match /quota/i.
  • storage.sync for binary / image data - the 100 KB total + 8 KB per-item caps aren't designed for blobs; use storage.local.
  • storage.session for cross-restart data - passes in a single session, fails in prod on cold start.
  • Hard-coding the storage.local limit - read the live constant, not a literal (5 MB vs 10 MB across Chrome versions).
  • Per mdn-storage, no area encrypts at rest ("Storage area is not encrypted, and shouldn't store confidential information").
  • Sync round-trip propagation needs two signed-in profiles - not testable in headless CI without account credentials.
  • Throughput caps are enforced server-side; tests can only assert the extension's own batching stays under the cap.

References

  • Chrome chrome.storage API reference (quotas, deprecations, quota-exceeded behavior) - cr-storage.
  • MDN WebExtensions storage API (Firefox semantics, managed read-only, encryption caveat) - mdn-storage.

SKILL.md

tile.json