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.
68
86%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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.
All constants per cr-storage:
| Area | QUOTA_BYTES | Per-item | Lifetime | Cross-device | Writer |
|---|---|---|---|---|---|
storage.local | 10,485,760 (10 MB) | none documented | until extension removal | no | extension |
storage.sync | 102,400 (~100 KB) | 8,192 (8 KB) | persistent, synced | yes (when user signed in) | extension |
storage.session | 10,485,760 (10 MB) | none documented | cleared on disable, reload, update, or browser restart; MV3-only | no | extension |
storage.managed | - | - | as long as policy is in effect | varies | admin 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 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);| Test | Asserts |
|---|---|
storage.sync total-quota | a 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 shape | listener receives (changes, areaName) with changes[key] = { oldValue?, newValue? } - same shape on Firefox per mdn-storage |
| multi-area isolation | a storage.local write is invisible to storage.sync.get |
storage.managed read-only | any managed.set rejects ("Trying to modify this namespace results in an error" per mdn-storage) |
| session lifetime | storage.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| Concern | Chrome | Firefox |
|---|---|---|
storage.sync quotas | 102,400 / 8,192 / 512 / 1,800 per hour per cr-storage | MDN 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-only | Yes per cr-storage | Yes per mdn-storage - skip session tests when targeting MV2 |
storage.managed | Available; admin-configured per OS | Available per mdn-storage; policy injection mechanism differs per OS |
| Sync sign-in | Chrome account required | Firefox 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.
set + get - quota silently drops past 8 KB /
100 KB; author the quota tests above./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.storage.local limit - read the live constant, not a
literal (5 MB vs 10 MB across Chrome versions).chrome.storage API reference (quotas, deprecations,
quota-exceeded behavior) - cr-storage.