# Playwright patterns for Google Docs formatting

These patterns are implementation notes, not guarantees about Google’s DOM. Re-inspect labels and selectors before each use.

## Launch and readiness

```js
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.goto(DOC_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(7000);
```

Useful indicators:

```js
await page.locator('[aria-label^="Document status:"]').first().getAttribute('aria-label');
await page.getByRole('listbox', { name: 'Styles' }).innerText();
```

A status such as `Document status: Saved to Drive.` is the final save check.

## Apply a heading to a precise outline item

Google Docs body text is canvas-based and ordinary text locators can be unreliable. The left outline exposes semantic labels such as `Section name level 3`.

```js
const item = page.locator(
  '.navigation-item-content-container[aria-label="Section name level 3"]'
).first();
await item.click();
await page.waitForTimeout(200);
await page.keyboard.press('Control+Alt+3');
```

Use `Control+Alt+1` through `Control+Alt+4` for heading levels. Re-export to verify, because the clicked outline item and active body paragraph can diverge after scrolling or document rerendering.

## Native TOC insertion

```js
await page.getByRole('menuitem', { name: 'Insert', exact: true }).click();
await page.getByText('Page elements', { exact: true }).hover();
await page.getByText('Table of contents', { exact: true }).hover();
const cells = page.locator('.kix-toc-palette [role=gridcell]');
await cells.nth(2).click(); // blue links, no page numbers (inspect aria-label first)
```

Inspect rather than assume the palette ordering:

```js
for (let i = 0; i < await cells.count(); i++) {
  console.log(i, await cells.nth(i).getAttribute('aria-label'));
}
```

## Restrict TOC to Heading 1 only

```js
// Click inside the TOC first.
await page.mouse.click(520, 300); // derive coordinates from a fresh screenshot
const toolbar = page.locator('[aria-label="Table of contents"]');
await toolbar.getByRole('button', { name: 'More options' }).click();
await page.getByRole('menuitem', { name: 'Table of contents options' }).click();
await page.getByRole('button', { name: 'Heading levels' }).click();

for (const n of [2, 3, 4]) {
  const checkbox = page.locator(`input[type=checkbox][aria-label="Heading ${n}"]`);
  if (await checkbox.isChecked()) await checkbox.click();
}
```

After the level settings change, the TOC may regenerate automatically. If the contextual toolbar remains visible, click:

```js
await toolbar.getByRole('button', { name: 'Update table of contents' }).click();
```

If it disappears, re-click inside the TOC before locating the toolbar.

## DOCX verification

The exported DOCX is a ZIP archive. Parse `word/document.xml`.

- Paragraph style is under `w:pPr/w:pStyle`.
- Text is the concatenation of descendant `w:t` nodes.
- A generated TOC normally appears as a `w:sdt` body child.

To verify a compact two-entry TOC, inspect text under the first `w:sdt` and require the two top-level headings with no assignment/subsection names.

When comparing content before/after formatting, omit `w:sdt` blocks so generated TOC text does not cause a false mismatch. Compare the remaining paragraph text and character count or SHA-256 checksum.

## Common recovery sequence

1. Take a fresh screenshot.
2. Click the TOC/body element again; old contextual toolbars may disappear.
3. Re-query accessible labels instead of reusing stale locators.
4. Wait for `Saved to Drive`.
5. Export and inspect DOCX.
6. Report only what the export proves.
