en un clic
e2e-testing
// Guide for writing, running, and debugging Playwright E2E tests for the TapWord Translator Chrome extension. Use this skill when creating new E2E tests or fixing failing tests.
// Guide for writing, running, and debugging Playwright E2E tests for the TapWord Translator Chrome extension. Use this skill when creating new E2E tests or fixing failing tests.
Guide for expert code review of Chrome Extensions (Manifest V3). Detects MV3 violations, security risks, performance issues, and architectural anti-patterns.
Comprehensive guide for all common GitHub operations via gh CLI: fetching issues and PRs, creating PRs, pushing branches, listing and filtering, and managing PR lifecycle. Use this when performing any GitHub task beyond basic git commands.
Generate a factual code change handoff manifest for reviewers. Use when asked to create a review checklist, review manifest, handoff manifest, or reviewer-oriented change audit. Keywords: review checklist, review manifest, handoff manifest, code change audit, reviewer context, risk checklist.
| name | e2e-testing |
| description | Guide for writing, running, and debugging Playwright E2E tests for the TapWord Translator Chrome extension. Use this skill when creating new E2E tests or fixing failing tests. |
This skill provides instructions for writing and running Playwright E2E tests for the TapWord Translator Chrome extension.
npm run build must be run before any E2E tests, otherwise Playwright will load an outdated or missing dist/ folder.npm run test:e2e:headed (Recommended for debugging)npm run test:e2e:headed -- tests/e2e/specs/your-test.spec.tstests/e2e/screenshots/ upon failure or manual capture.When creating new specs in tests/e2e/specs/, you MUST follow these architectural patterns to avoid common extension-testing pitfalls.
Always force zh-CN locale. This ensures the extension defaults targetLanguage to zh (Chinese) without needing manual storage seeding.
const EXTENSION_ENABLED_FLAGS = [
'--enable-unsafe-extension-debugging',
'--disable-features=DisableLoadExtensionCommandLineSwitch',
'--disable-extensions-except=' + EXTENSION_DIST_PATH,
'--load-extension=' + EXTENSION_DIST_PATH,
'--lang=zh-CN', // Force locale to Chinese so targetLanguage defaults to 'zh'
];
const context = await chromium.launchPersistentContext(userDataDir, {
headless: false,
args: EXTENSION_ENABLED_FLAGS,
locale: 'zh-CN', // Set Playwright context locale as well
});
The extension has two layers of initialization. You must wait for both:
Do NOT use waitForTimeout(2000) alone.
// 1. Wait for Service Worker
await waitForExtensionServiceWorker(context);
// 2. Wait for Content Script (check for injected CSS variable)
await page.waitForFunction(() => {
const val = getComputedStyle(document.documentElement).getPropertyValue('--ai-translator-underline-offset');
return val && val.trim() !== '';
}, null, { timeout: 8000 });
chrome.*)The test page (page) is a regular web page and cannot access chrome.* APIs. You must run these in the Service Worker context.
❌ Wrong:
await page.evaluate(() => chrome.storage.sync.set({ ... })); // Throws Error
✅ Correct:
const worker = context.serviceWorkers().find(w => w.url().startsWith('chrome-extension://'));
await worker.evaluate(() => chrome.storage.sync.set({ ... }));
// RELOAD page after changing settings so content script picks them up!
await page.reload();
The extension has two independent trigger paths, and the correct approach depends on which one is active:
singleClickTranslate (default ON): click a word directlyThis is the most reliable path for tests. The extension's handleSingleClick handler listens for trusted click events and detects the word at the cursor position automatically.
For local HTML fixtures (word wrapped in <span id="target-word">):
await page.locator('#target-word').click(); // trusted click → handleSingleClick fires
For real websites (no id wrapper available), find the text node's bounding rect first:
const clickPoint = await page.evaluate((phrase) => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let node: Node | null;
while ((node = walker.nextNode())) {
const idx = (node as Text).nodeValue?.indexOf(phrase) ?? -1;
if (idx !== -1) {
const range = document.createRange();
range.setStart(node as Text, idx);
range.setEnd(node as Text, idx + phrase.split(' ')[0].length); // click the first word
const r = range.getBoundingClientRect();
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
}
}
return null;
}, 'target phrase');
if (clickPoint) {
await page.mouse.click(clickPoint.x, clickPoint.y); // trusted physical click
}
Why not
page.evaluate(() => el.dispatchEvent(new MouseEvent('click',...)))? Events created withnew MouseEvent()in the browser haveisTrusted === false. The extension's click handler does NOT explicitly checkisTrusted, butpage.locator.dispatchEvent()and inlineelement.dispatchEvent()create synthetic events. Usingpage.mouse.click()orpage.locator().click()always produces trusted events and is the safe choice.
When showIcon: true and singleClickTranslate: false, the user must drag-select text first and then click the floating icon.
Use page.mouse for a trusted drag — do NOT use the JS Selection API + synthetic mouseup, because the async handleTextSelection handler may read an already-collapsed selection:
// Get start/end coordinates of the text span
const box = await page.locator('#target-word').boundingBox();
await page.mouse.move(box!.x, box!.y + box!.height / 2);
await page.mouse.down();
await page.mouse.move(box!.x + box!.width, box!.y + box!.height / 2);
await page.mouse.up(); // trusted mouseup → handleTextSelection fires
// Wait for icon, then click it
const icon = page.locator('.ai-translator-icon').first();
await expect(icon).toBeVisible({ timeout: 5_000 });
await icon.click();
Attach listeners immediately after creating the page.
page.on('console', msg => console.log(`[PAGE ${msg.type().toUpperCase()}] ${msg.text()}`));
page.on('pageerror', err => console.log(`[PAGE ERROR] ${err.message}`));
Limitation: Playwright cannot capture console.log from MV3 Service Workers in Chrome/Edge channels.
worker.on('close') to detect crashes/restarts.waitForExtensionServiceWorkerasync function waitForExtensionServiceWorker(context: any): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < 15000) {
const worker = context.serviceWorkers().find(w => w.url().startsWith('chrome-extension://'));
if (worker) return worker.url();
await new Promise(r => setTimeout(r, 500));
}
return '';
}
tests/html/.<span id="target-word"> to wrap test targets for precise selection.createLocalHtmlServer() to serve them (file:// protocol is often restricted).Different pages use different scroll models. A naive window.scrollBy() will silently do nothing on sites where only an inner container scrolls (e.g. OpenAI docs, many Next.js / SPA layouts where html and body have overflow: hidden).
Universal approach — use page.mouse.wheel():
// Works for both window scroll AND inner-container scroll
await page.mouse.wheel(0, 200); // scroll 200px down
await page.waitForTimeout(120); // let extension's rAF-debounced repositioning run
When window.scrollBy() is appropriate (only for pages you fully control, e.g. local HTML fixtures where body is the scroll root):
await page.evaluate(() => window.scrollBy(0, 200));
When scrolling an explicit container (local fixtures like issue-35-container-scroll.html):
await page.evaluate(() =>
document.getElementById('scroll-container')!.scrollBy(0, 200)
);
Scroll amount matters for tooltip drift tests: Use small steps (≤ 100 px) so the anchor element stays inside the viewport. Large steps (300 px+) will push the anchor off-screen, making drift impossible to observe visually.
chrome.storage?test.setTimeout(120_000) for long tests?context in a finally block?screenshot({ clip: ... })) to avoid viewport scrolling issues?page.mouse.click() or page.locator().click() (trusted events) instead of element.dispatchEvent() (untrusted, unreliable)?page.mouse.wheel() instead of window.scrollBy()?