diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 827b168..f8b3e71 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -74,6 +74,19 @@ make mend-scan
- `RENOVATE_TOKEN_FILE`
- `gh auth token` (GitHub CLI fallback)
+## Renderer CSP Policy
+
+- The renderer HTML (`src/renderer/public/index.html`) defines a strict CSP via
+ ``.
+- Policy baseline:
+ - `script-src 'self'`
+ - `style-src 'self'`
+ - `object-src 'none'`
+ - `base-uri 'none'`
+- No `'unsafe-inline'` or `'unsafe-eval'` exceptions are allowed.
+- Dark mode bootstrap is intentionally externalized to `src/renderer/public/theme-bootstrap.js`
+ so startup theme logic works without inline scripts.
+
## Manual Setup (Without Make)
```bash
diff --git a/jest.config.js b/jest.config.js
index 9925390..f378a18 100755
--- a/jest.config.js
+++ b/jest.config.js
@@ -22,6 +22,7 @@ module.exports = {
coverageDirectory: '/coverage',
collectCoverageFrom: [
'/src/renderer/components/**/*.{js,jsx,ts,tsx}',
+ '/src/renderer/public/**/*.js',
'/src/main/index.ts',
'/src/main/preload.ts',
'/src/main/security/navigation-guard.ts',
diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js
index b1b6fc9..6ab9a46 100644
--- a/scripts/capture-ui-screenshot.js
+++ b/scripts/capture-ui-screenshot.js
@@ -17,6 +17,15 @@ const DEFAULT_SCREENSHOT_NAME = `ui-${process.platform}-${process.arch}.png`;
const DEFAULT_LOCALE = 'en';
const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de'];
const FIXED_MTIME = 1700000000000;
+const QA_DISABLE_ANIMATIONS_STYLESHEET_PATH = '/__qa__/disable-animations.css';
+const QA_DISABLE_ANIMATIONS_STYLESHEET_CONTENT = `
+*, *::before, *::after {
+ transition-duration: 0s !important;
+ animation-duration: 0s !important;
+ animation-delay: 0s !important;
+ scroll-behavior: auto !important;
+}
+`;
function loadSecretScannerHelpers() {
const compiledSecretScannerPath = path.join(
@@ -133,6 +142,22 @@ function resolveFilePath(requestUrl) {
function createStaticServer() {
return http.createServer((request, response) => {
+ let requestPath;
+
+ try {
+ requestPath = decodeURIComponent((request.url || '/').split('?')[0]);
+ } catch {
+ response.writeHead(400, { 'Content-Type': 'text/plain; charset=UTF-8' });
+ response.end('Bad Request');
+ return;
+ }
+
+ if (requestPath === QA_DISABLE_ANIMATIONS_STYLESHEET_PATH) {
+ response.writeHead(200, { 'Content-Type': 'text/css; charset=UTF-8' });
+ response.end(QA_DISABLE_ANIMATIONS_STYLESHEET_CONTENT);
+ return;
+ }
+
const requestedPath = resolveFilePath(request.url || '/');
if (!requestedPath) {
@@ -575,16 +600,24 @@ async function captureLocaleScreenshots(page) {
async function captureAppStateScreenshots(page) {
await runStep('Disable animations for stable screenshots', async () => {
- await page.addStyleTag({
- content: `
- *, *::before, *::after {
- transition-duration: 0s !important;
- animation-duration: 0s !important;
- animation-delay: 0s !important;
- scroll-behavior: auto !important;
- }
- `,
- });
+ await page.evaluate(async (stylesheetHref) => {
+ const existingStylesheet = document.querySelector('link[data-qa-disable-animations="true"]');
+ if (existingStylesheet instanceof HTMLLinkElement) {
+ return;
+ }
+
+ await new Promise((resolve, reject) => {
+ const stylesheetLink = document.createElement('link');
+ stylesheetLink.rel = 'stylesheet';
+ stylesheetLink.href = stylesheetHref;
+ stylesheetLink.setAttribute('data-qa-disable-animations', 'true');
+ stylesheetLink.onload = resolve;
+ stylesheetLink.onerror = () => {
+ reject(new Error('Failed to load QA disable-animations stylesheet'));
+ };
+ document.head.appendChild(stylesheetLink);
+ });
+ }, QA_DISABLE_ANIMATIONS_STYLESHEET_PATH);
});
await runStep('Wait for app root', async () => {
diff --git a/src/renderer/public/index.html b/src/renderer/public/index.html
index cdbaf09..e3e7a40 100755
--- a/src/renderer/public/index.html
+++ b/src/renderer/public/index.html
@@ -3,23 +3,13 @@
+
AI Code Fusion
-
+
realFs.readFileSync(rendererIndexPath, 'utf8');
+
+describe('renderer CSP policy', () => {
+ it('defines a strict CSP policy without unsafe script/style exceptions', () => {
+ const indexHtml = readRendererIndex();
+ const cspMatch = indexHtml.match(
+ / {
+ const indexHtml = readRendererIndex();
+
+ expect(indexHtml).toContain('');
+
+ const inlineScriptTags = [...indexHtml.matchAll(/