feat: improve HTML rewriting - remove decorative subtrees and aria-hidden elements
- Add removeAriaHiddenPlugin to strip aria-hidden=true subtrees - Add removeEmptyAltImagesPlugin to remove decorative images - Add removeDecorativeSubtreesPlugin to prune content-free branches - Update test snapshots for x.com
This commit is contained in:
+86
-7
@@ -201,6 +201,29 @@ await page.waitForSelector('article', { timeout: 10000 });
|
||||
await waitForPageLoad({ page, timeout: 5000 });
|
||||
```
|
||||
|
||||
**7. Login buttons that open popups**
|
||||
Playwriter extension cannot control popup windows. If a login button opens a popup (common with OAuth/SSO), use cmd+click to open in a new tab instead:
|
||||
```js
|
||||
// BAD: popup window is not controllable by playwriter
|
||||
await page.click('button:has-text("Login with Google")');
|
||||
|
||||
// GOOD: cmd+click opens in new tab that playwriter can control
|
||||
await page.locator('button:has-text("Login with Google")').click({ modifiers: ['Meta'] });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Verify new tab opened - last page should be the login page
|
||||
const pages = context.pages();
|
||||
const loginPage = pages[pages.length - 1];
|
||||
if (loginPage.url() === page.url()) {
|
||||
throw new Error('Cmd+click did not open new tab - login may have opened as popup');
|
||||
}
|
||||
|
||||
// Complete login flow in loginPage, cookies are shared with original page
|
||||
await loginPage.locator('[data-email]').first().click();
|
||||
await loginPage.waitForURL('**/callback**');
|
||||
// Original page should now be authenticated
|
||||
```
|
||||
|
||||
## checking page state
|
||||
|
||||
After any action (click, submit, navigate), verify what happened:
|
||||
@@ -350,13 +373,58 @@ await waitForPageLoad({ page, timeout: 5000 });
|
||||
|
||||
## common patterns
|
||||
|
||||
**Popups** - capture before triggering:
|
||||
**Authenticated fetches** - to access protected resources, fetch from within page context (includes session cookies automatically):
|
||||
|
||||
```js
|
||||
const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]);
|
||||
await popup.waitForLoadState(); console.log('Popup URL:', popup.url());
|
||||
// BAD: curl/external requests don't have session cookies
|
||||
// curl -H "Cookie: ..." often fails due to missing cookies or CSRF
|
||||
|
||||
// GOOD: fetch inside page.evaluate uses browser's full session
|
||||
const data = await page.evaluate(async (url) => {
|
||||
const resp = await fetch(url);
|
||||
return await resp.text();
|
||||
}, 'https://example.com/protected/resource');
|
||||
```
|
||||
|
||||
**Downloading large data** - console output truncates large strings. Trigger a browser download instead:
|
||||
|
||||
```js
|
||||
// Fetch protected data and trigger download to user's Downloads folder
|
||||
await page.evaluate(async (url) => {
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.text();
|
||||
const blob = new Blob([data], { type: 'application/octet-stream' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'data.json';
|
||||
a.click();
|
||||
}, 'https://example.com/protected/large-file');
|
||||
// File saves to ~/Downloads - read it from there
|
||||
```
|
||||
|
||||
**Avoid permission-gated browser APIs** - some APIs require user permission prompts or special browser flags. These often fail silently or hang. Examples to avoid:
|
||||
- `navigator.clipboard.writeText()` - requires permission
|
||||
- Multiple concurrent downloads - browser may block
|
||||
- `window.showSaveFilePicker()` - requires user gesture
|
||||
- Geolocation, camera, microphone APIs
|
||||
|
||||
Instead, use simpler alternatives (single download via `a.click()`, store data in `state`, etc).
|
||||
|
||||
**Links that open new tabs** - use cmd+click to open in a controllable new tab:
|
||||
|
||||
```js
|
||||
// For links with target=_blank or buttons that open popups
|
||||
await page.locator('a[target=_blank]').click({ modifiers: ['Meta'] });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// New tab is last in context.pages()
|
||||
const pages = context.pages();
|
||||
const newTab = pages[pages.length - 1];
|
||||
console.log('New tab URL:', newTab.url());
|
||||
```
|
||||
|
||||
Note: `page.waitForEvent('popup')` is unreliable - playwriter cannot control popup windows opened via `window.open`. Use cmd+click instead.
|
||||
|
||||
**Downloads** - capture and save:
|
||||
|
||||
```js
|
||||
@@ -401,12 +469,23 @@ const html = await getCleanHTML({ locator: page, search: /button/i })
|
||||
const diff = await getCleanHTML({ locator: page, showDiffSinceLastCall: true })
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `locator` - Playwright Locator or Page to get HTML from
|
||||
- `search` - string/regex to filter results (returns first 10 matching lines)
|
||||
- `showDiffSinceLastCall` - returns diff since last snapshot
|
||||
- `search` - string/regex to filter results (returns first 10 matching lines with 5 lines context)
|
||||
- `showDiffSinceLastCall` - returns unified diff since last call for same locator/page. First call stores snapshot and returns message; subsequent calls return the diff. Useful for tracking DOM changes after actions.
|
||||
- `includeStyles` - keep style and class attributes (default: false)
|
||||
|
||||
Returns cleaned HTML with only essential attributes (aria-*, data-*, href, role, title, alt, etc.). Removes script, style, svg, head tags.
|
||||
**HTML processing:**
|
||||
The function cleans HTML for compact, readable output:
|
||||
- **Removes tags**: script, style, link, meta, noscript, svg, head
|
||||
- **Unwraps nested wrappers**: Empty divs/spans with no attributes that only wrap a single child are collapsed (e.g., `<div><div><div><p>text</p></div></div></div>` → `<div><p>text</p></div>`)
|
||||
- **Removes empty elements**: Elements with no attributes and no content are removed
|
||||
- **Truncates long values**: Attribute values >200 chars and text content >500 chars are truncated
|
||||
|
||||
**Attributes kept (summary):**
|
||||
- Common semantic and ARIA attributes (e.g., `href`, `name`, `type`, `aria-*`)
|
||||
- All `data-*` test attributes
|
||||
- Frequently used test IDs and special attributes (e.g., `testid`, `qa`, `e2e`, `vimium-label`)
|
||||
|
||||
For pagination, use `.split('\n').slice(offset, offset + limit).join('\n')`:
|
||||
```js
|
||||
@@ -485,7 +564,7 @@ await screenshotWithAccessibilityLabels({ page });
|
||||
|
||||
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
|
||||
|
||||
**startRecording / stopRecording** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` in the extension context, so **recording survives page navigation**. Video is saved as WebM.
|
||||
**startRecording / stopRecording** - record the page as a video at native FPS (30-60fps). Uses `chrome.tabCapture` in the extension context, so **recording survives page navigation**. Video is saved as mp4.
|
||||
|
||||
**Note**: Recording requires the user to have clicked the Playwriter extension icon on the tab. This grants `activeTab` permission needed for `chrome.tabCapture`. Recording works on tabs where the icon was clicked - if you need to record a new tab, ask the user to click the icon on it first.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user