Depending on what you are changing, you may need to run the build processes locally, or individual tests. The following all run within GitHub Actions when you create a pull request, but you can run them locally as well.
If you want to get a good feeling for whether a PR or CI run will pass/fail, you can run the test command which chains most of the following together:
# run this if you want some confidence that your PR will pass
npm test
See root-level package for lint commands
See root-level package for TypeScript commands
Everything for unit-testing is located in the unit-test folder. Jasmine configuration is in unit-test/jasmine.json.
npm run test-unit
Everything within integration-test is integration tests controlled by Playwright.
npm run test-int
Important: When writing integration tests, follow the testing best practices outlined in the Test Pages Guide. These guidelines cover avoiding custom state in spec files, using platform configuration, and preferring config-driven testing approaches.
Preferred Testing Approach: The Test Pages Guide describes the most preferred type of testing for the /injected directory. Test pages are the preferred approach where possible because they are sharable with platforms - the same test pages can be used by Android, Apple, Windows, and browser extension teams, ensuring consistent functionality validation across all platforms.
To produce all artefacts that are used by platforms, just run the npm run build command. This will create platform specific code within the build folder (that is not checked in).
npm run build
Test builds are created with a GitHub workflow. The assets for Content Scope Scripts will be created on demand if they are absent (which they will be, if you're pointing to a branch of C-S-S).
If you drop a debugger; line in the scripts and open DevTools window, the DevTools will breakpoint and navigate to that exact line in code when the debug point has been hit.
Open DevTools, go to the Console tab and enter navigator.duckduckgo. If it's defined, then Content Scope Scripts is running.
Playwright tests must be independent for safe parallel execution:
// ✅ Each test is self-contained
test('overlay displays correctly', async ({ page }) => {
const overlays = OverlaysPage.create(page);
await overlays.openPage(url);
await expect(overlays.element).toBeVisible();
});
// ❌ Avoid shared state between tests
let sharedPage; // Don't do this
Use Playwright's test fixtures for complex setup.
Always await async operations—missing await causes flaky tests:
// ✅ Correct
await overlays.opensShort(url);
// ❌ Test passes incorrectly (promise not awaited)
overlays.opensShort(url);
Use specific patterns to avoid including non-test files:
// ✅ Correct - only .spec.js files
'integration-test/**/*.spec.js';
// ❌ Incorrect - includes config, fixtures, etc.
'integration-test/**';
Match test setup to the feature state being tested:
// Testing disabled state
test('feature disabled shows fallback', async ({ page }) => {
await page.addInitScript(() => {
window.__ddg_config__ = { features: { myFeature: { state: 'disabled' } } };
});
// ...
});
Extract DOM manipulation and HTML generation to separate files for focused unit testing. This prevents XSS vulnerabilities from slipping through integration-only testing.
integration-test/
├── test-pages/
│ └── <feature-name>/
│ ├── config/
│ │ └── config.json # Feature config fixtures
│ ├── pages/
│ │ └── test-page.html # Test HTML pages
│ └── <feature-name>.spec.js # Playwright tests
Create config fixtures that match privacy-configuration schema:
{
"features": {
"featureName": {
"state": "enabled",
"settings": {
"settingName": "value",
"conditionalChanges": [
{
"condition": { "domain": "localhost" },
"patchSettings": [{ "op": "replace", "path": "/settingName", "value": "testValue" }]
}
]
}
}
}
}
test('feature behavior', async ({ page }) => {
await page.goto('/test-pages/feature/pages/test.html');
// Wait for feature initialization
await page.waitForFunction(() => window.__ddg_feature_ready__);
// Test feature behavior
const result = await page.evaluate(() => someAPI());
expect(result).toBe(expectedValue);
});
For conditional changes and config schema details, see the injected cursor rules.