@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Browser Extensions
|
|
2
|
+
|
|
3
|
+
> **When to use**: Testing Chrome extensions — popups, content scripts, background service workers, or extension-injected UI. Requires Chromium and a persistent browser context.
|
|
4
|
+
> **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Load an unpacked extension with a persistent context (Chromium only)
|
|
10
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
11
|
+
headless: false, // Extensions require headed mode
|
|
12
|
+
args: [
|
|
13
|
+
`--disable-extensions-except=${pathToExtension}`,
|
|
14
|
+
`--load-extension=${pathToExtension}`,
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Hard constraints**: Extensions only work in Chromium. They require `launchPersistentContext` (not `browser.newContext`). Headed mode is mandatory — `headless: false` or use the `--headless=new` Chromium flag for "new headless" mode which supports extensions.
|
|
20
|
+
|
|
21
|
+
## Patterns
|
|
22
|
+
|
|
23
|
+
### Loading an Extension
|
|
24
|
+
|
|
25
|
+
**Use when**: You need to test any Chrome extension functionality.
|
|
26
|
+
**Avoid when**: You only need to test the web app that an extension interacts with — mock the extension's effects instead.
|
|
27
|
+
|
|
28
|
+
**TypeScript**
|
|
29
|
+
```typescript
|
|
30
|
+
import { test as base, expect, chromium, type BrowserContext } from '@playwright/test';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
|
|
33
|
+
// Create a fixture that provides a context with the extension loaded
|
|
34
|
+
type ExtensionFixtures = {
|
|
35
|
+
context: BrowserContext;
|
|
36
|
+
extensionId: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const test = base.extend<ExtensionFixtures>({
|
|
40
|
+
// Override the default context to load the extension
|
|
41
|
+
context: async ({}, use) => {
|
|
42
|
+
const extensionPath = path.resolve(__dirname, '../my-extension');
|
|
43
|
+
const context = await chromium.launchPersistentContext('', {
|
|
44
|
+
headless: false,
|
|
45
|
+
args: [
|
|
46
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
47
|
+
`--load-extension=${extensionPath}`,
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
await use(context);
|
|
51
|
+
await context.close();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Extract the extension ID from the service worker URL
|
|
55
|
+
extensionId: async ({ context }, use) => {
|
|
56
|
+
let [background] = context.serviceWorkers();
|
|
57
|
+
if (!background) {
|
|
58
|
+
background = await context.waitForEvent('serviceworker');
|
|
59
|
+
}
|
|
60
|
+
const extensionId = background.url().split('/')[2];
|
|
61
|
+
await use(extensionId);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export { expect };
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**JavaScript**
|
|
69
|
+
```javascript
|
|
70
|
+
const { test: base, expect, chromium } = require('@playwright/test');
|
|
71
|
+
const path = require('path');
|
|
72
|
+
|
|
73
|
+
const test = base.extend({
|
|
74
|
+
context: async ({}, use) => {
|
|
75
|
+
const extensionPath = path.resolve(__dirname, '../my-extension');
|
|
76
|
+
const context = await chromium.launchPersistentContext('', {
|
|
77
|
+
headless: false,
|
|
78
|
+
args: [
|
|
79
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
80
|
+
`--load-extension=${extensionPath}`,
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
await use(context);
|
|
84
|
+
await context.close();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
extensionId: async ({ context }, use) => {
|
|
88
|
+
let [background] = context.serviceWorkers();
|
|
89
|
+
if (!background) {
|
|
90
|
+
background = await context.waitForEvent('serviceworker');
|
|
91
|
+
}
|
|
92
|
+
const extensionId = background.url().split('/')[2];
|
|
93
|
+
await use(extensionId);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
module.exports = { test, expect };
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Testing Extension Popups
|
|
101
|
+
|
|
102
|
+
**Use when**: Your extension has a browser action popup (the UI that appears when clicking the extension icon).
|
|
103
|
+
**Avoid when**: The popup is trivial — test the content script or background logic instead.
|
|
104
|
+
|
|
105
|
+
**TypeScript**
|
|
106
|
+
```typescript
|
|
107
|
+
import { test, expect } from './extension-fixture';
|
|
108
|
+
|
|
109
|
+
test('extension popup displays saved bookmarks', async ({ page, extensionId }) => {
|
|
110
|
+
// Navigate directly to the popup HTML
|
|
111
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
112
|
+
|
|
113
|
+
// Interact with popup UI using standard locators
|
|
114
|
+
await expect(page.getByRole('heading', { name: 'My Bookmarks' })).toBeVisible();
|
|
115
|
+
await page.getByRole('button', { name: 'Add current page' }).click();
|
|
116
|
+
await expect(page.getByRole('listitem')).toHaveCount(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('extension popup settings toggle works', async ({ page, extensionId }) => {
|
|
120
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
121
|
+
|
|
122
|
+
await page.getByRole('checkbox', { name: 'Enable notifications' }).check();
|
|
123
|
+
await expect(page.getByText('Notifications enabled')).toBeVisible();
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**JavaScript**
|
|
128
|
+
```javascript
|
|
129
|
+
const { test, expect } = require('./extension-fixture');
|
|
130
|
+
|
|
131
|
+
test('extension popup displays saved bookmarks', async ({ page, extensionId }) => {
|
|
132
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
133
|
+
|
|
134
|
+
await expect(page.getByRole('heading', { name: 'My Bookmarks' })).toBeVisible();
|
|
135
|
+
await page.getByRole('button', { name: 'Add current page' }).click();
|
|
136
|
+
await expect(page.getByRole('listitem')).toHaveCount(1);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Testing Content Scripts
|
|
141
|
+
|
|
142
|
+
**Use when**: Your extension injects scripts or UI into web pages.
|
|
143
|
+
**Avoid when**: The content script only modifies data without visible effects — test via the background worker or storage.
|
|
144
|
+
|
|
145
|
+
**TypeScript**
|
|
146
|
+
```typescript
|
|
147
|
+
import { test, expect } from './extension-fixture';
|
|
148
|
+
|
|
149
|
+
test('content script injects price comparison widget', async ({ context }) => {
|
|
150
|
+
const page = await context.newPage();
|
|
151
|
+
await page.goto('https://example-shop.com/product/123');
|
|
152
|
+
|
|
153
|
+
// Wait for the content script to inject its UI
|
|
154
|
+
// The extension adds a shadow DOM element — Playwright pierces it automatically
|
|
155
|
+
await expect(page.getByTestId('price-compare-widget')).toBeVisible({ timeout: 10000 });
|
|
156
|
+
await expect(page.getByText('Best price: $29.99')).toBeVisible();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('content script highlights search terms', async ({ context }) => {
|
|
160
|
+
const page = await context.newPage();
|
|
161
|
+
await page.goto('https://example.com/article');
|
|
162
|
+
|
|
163
|
+
// Verify the content script added highlight spans
|
|
164
|
+
const highlights = page.locator('.ext-highlight');
|
|
165
|
+
await expect(highlights).toHaveCount(5);
|
|
166
|
+
await expect(highlights.first()).toHaveCSS('background-color', 'rgb(255, 255, 0)');
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**JavaScript**
|
|
171
|
+
```javascript
|
|
172
|
+
const { test, expect } = require('./extension-fixture');
|
|
173
|
+
|
|
174
|
+
test('content script injects price comparison widget', async ({ context }) => {
|
|
175
|
+
const page = await context.newPage();
|
|
176
|
+
await page.goto('https://example-shop.com/product/123');
|
|
177
|
+
|
|
178
|
+
await expect(page.getByTestId('price-compare-widget')).toBeVisible({ timeout: 10000 });
|
|
179
|
+
await expect(page.getByText('Best price: $29.99')).toBeVisible();
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Testing Background Service Workers
|
|
184
|
+
|
|
185
|
+
**Use when**: Your extension uses Manifest V3 service workers for background processing, alarms, or message passing.
|
|
186
|
+
**Avoid when**: The background logic is simple and already covered by popup or content script tests.
|
|
187
|
+
|
|
188
|
+
**TypeScript**
|
|
189
|
+
```typescript
|
|
190
|
+
import { test, expect } from './extension-fixture';
|
|
191
|
+
|
|
192
|
+
test('background worker processes messages correctly', async ({ context, extensionId }) => {
|
|
193
|
+
const page = await context.newPage();
|
|
194
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
195
|
+
|
|
196
|
+
// Trigger an action that sends a message to the background worker
|
|
197
|
+
await page.getByRole('button', { name: 'Sync data' }).click();
|
|
198
|
+
|
|
199
|
+
// Verify the response from the background worker updates the popup
|
|
200
|
+
await expect(page.getByText('Last synced: just now')).toBeVisible();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('service worker handles extension storage', async ({ context, extensionId }) => {
|
|
204
|
+
const page = await context.newPage();
|
|
205
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
206
|
+
|
|
207
|
+
// Set a value through the popup
|
|
208
|
+
await page.getByLabel('API Key').fill('test-key-123');
|
|
209
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
210
|
+
|
|
211
|
+
// Reload popup and verify persistence through the service worker
|
|
212
|
+
await page.reload();
|
|
213
|
+
await expect(page.getByLabel('API Key')).toHaveValue('test-key-123');
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**JavaScript**
|
|
218
|
+
```javascript
|
|
219
|
+
const { test, expect } = require('./extension-fixture');
|
|
220
|
+
|
|
221
|
+
test('background worker processes messages correctly', async ({ context, extensionId }) => {
|
|
222
|
+
const page = await context.newPage();
|
|
223
|
+
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
|
224
|
+
|
|
225
|
+
await page.getByRole('button', { name: 'Sync data' }).click();
|
|
226
|
+
await expect(page.getByText('Last synced: just now')).toBeVisible();
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Testing Extension Options Page
|
|
231
|
+
|
|
232
|
+
**Use when**: Your extension has a dedicated options/settings page.
|
|
233
|
+
**Avoid when**: Settings are fully covered by popup tests.
|
|
234
|
+
|
|
235
|
+
**TypeScript**
|
|
236
|
+
```typescript
|
|
237
|
+
import { test, expect } from './extension-fixture';
|
|
238
|
+
|
|
239
|
+
test('options page saves preferences', async ({ page, extensionId }) => {
|
|
240
|
+
await page.goto(`chrome-extension://${extensionId}/options.html`);
|
|
241
|
+
|
|
242
|
+
await page.getByRole('combobox', { name: 'Theme' }).selectOption('dark');
|
|
243
|
+
await page.getByRole('checkbox', { name: 'Auto-update' }).check();
|
|
244
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
245
|
+
|
|
246
|
+
await expect(page.getByText('Settings saved')).toBeVisible();
|
|
247
|
+
|
|
248
|
+
// Verify persistence after reload
|
|
249
|
+
await page.reload();
|
|
250
|
+
await expect(page.getByRole('combobox', { name: 'Theme' })).toHaveValue('dark');
|
|
251
|
+
await expect(page.getByRole('checkbox', { name: 'Auto-update' })).toBeChecked();
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**JavaScript**
|
|
256
|
+
```javascript
|
|
257
|
+
const { test, expect } = require('./extension-fixture');
|
|
258
|
+
|
|
259
|
+
test('options page saves preferences', async ({ page, extensionId }) => {
|
|
260
|
+
await page.goto(`chrome-extension://${extensionId}/options.html`);
|
|
261
|
+
|
|
262
|
+
await page.getByRole('combobox', { name: 'Theme' }).selectOption('dark');
|
|
263
|
+
await page.getByRole('checkbox', { name: 'Auto-update' }).check();
|
|
264
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
265
|
+
|
|
266
|
+
await expect(page.getByText('Settings saved')).toBeVisible();
|
|
267
|
+
|
|
268
|
+
await page.reload();
|
|
269
|
+
await expect(page.getByRole('combobox', { name: 'Theme' })).toHaveValue('dark');
|
|
270
|
+
await expect(page.getByRole('checkbox', { name: 'Auto-update' })).toBeChecked();
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Decision Guide
|
|
275
|
+
|
|
276
|
+
| Scenario | Approach | Why |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| Test popup UI | Navigate to `chrome-extension://<id>/popup.html` | Direct access without needing to click the extension icon |
|
|
279
|
+
| Test content script effects | Load a real or test page, assert injected elements | Content scripts run automatically on matching URLs |
|
|
280
|
+
| Test background logic | Trigger via popup/content script, verify side effects | Cannot directly call service worker functions from Playwright |
|
|
281
|
+
| Test extension storage | Use popup to set values, reload, verify persistence | `chrome.storage` is only accessible from extension pages |
|
|
282
|
+
| Test options page | Navigate to `chrome-extension://<id>/options.html` | Same approach as popup testing |
|
|
283
|
+
| Test cross-page behavior | Open multiple pages in the same context | Persistent context shares extension state across tabs |
|
|
284
|
+
| Run in CI (headless) | Use `--headless=new` Chromium flag | New headless mode supports extensions unlike old headless |
|
|
285
|
+
| Test with multiple extensions | Add multiple paths to `--load-extension` | Comma-separate paths in the flag value |
|
|
286
|
+
|
|
287
|
+
## Anti-Patterns
|
|
288
|
+
|
|
289
|
+
| Don't Do This | Problem | Do This Instead |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `browser.newContext()` for extensions | Extensions require persistent context | `chromium.launchPersistentContext()` |
|
|
292
|
+
| `headless: true` without `--headless=new` | Old headless mode does not support extensions | Set `headless: false` or use `args: ['--headless=new']` |
|
|
293
|
+
| Testing on Firefox or WebKit | Extensions only work in Chromium | Skip extension tests for non-Chromium projects |
|
|
294
|
+
| Clicking the extension icon via coordinates | Fragile, toolbar layout varies | Navigate directly to `chrome-extension://<id>/popup.html` |
|
|
295
|
+
| Hardcoding the extension ID | IDs change between builds and machines | Extract dynamically from the service worker URL |
|
|
296
|
+
| Testing packed `.crx` files directly | Harder to debug, need to unpack first | Test the unpacked extension source directory |
|
|
297
|
+
| Sharing persistent context user data dir | State leaks between test runs | Use an empty string `''` for a temp directory |
|
|
298
|
+
| No timeout on content script assertions | Content scripts may load after page load | Use `{ timeout: 10000 }` on content script element assertions |
|
|
299
|
+
|
|
300
|
+
## Troubleshooting
|
|
301
|
+
|
|
302
|
+
| Symptom | Likely Cause | Fix |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| Extension does not load | Wrong path in `--load-extension` | Use `path.resolve()` to get the absolute path to the extension directory |
|
|
305
|
+
| `context.serviceWorkers()` returns empty | Service worker not yet registered | Use `context.waitForEvent('serviceworker')` before extracting the ID |
|
|
306
|
+
| Popup page is blank | Popup HTML path is wrong | Check `manifest.json` for the correct `default_popup` path |
|
|
307
|
+
| Content script not injecting | Page URL does not match `matches` in manifest | Verify the URL pattern in `content_scripts[].matches` |
|
|
308
|
+
| Extension works locally but not in CI | CI uses old headless mode | Add `--headless=new` to launch args for CI |
|
|
309
|
+
| `chrome.storage` calls fail | Accessing storage from non-extension context | Only access storage through extension pages (popup, options, background) |
|
|
310
|
+
| Multiple extensions conflict | Both extensions modify the same page elements | Test each extension in its own persistent context |
|
|
311
|
+
| Tests are slow to start | Persistent context initialization overhead | Reuse context across tests in the same file with `test.describe` |
|
|
312
|
+
|
|
313
|
+
## Related
|
|
314
|
+
|
|
315
|
+
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- building custom fixtures for extension contexts
|
|
316
|
+
- [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- service worker testing patterns (non-extension)
|
|
317
|
+
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- content scripts often inject Shadow DOM elements
|
|
318
|
+
- [core/configuration.md](configuration.md) -- project configuration for Chromium-only test suites
|
|
319
|
+
- [ci/ci-github-actions.md](../ci/ci-github-actions.md) -- CI setup for headed/extension tests
|