Skip to content

Commit abe4d9a

Browse files
Merge branch 'main' into dependabot/github_actions/main/peter-evans/find-comment-4
2 parents ea591a2 + 0137fff commit abe4d9a

File tree

13 files changed

+561
-122
lines changed

13 files changed

+561
-122
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { test, expect } from '@playwright/test';
2+
import { ResultsCollector } from './page-objects/results-collector.js';
3+
4+
const HTML = '/duck-ai-data-clearing/index.html';
5+
const CONFIG = './integration-test/test-pages/duck-ai-data-clearing/config/enabled.json';
6+
7+
test('duck-ai-data-clearing feature clears localStorage and IndexedDB', async ({ page }, testInfo) => {
8+
const collector = ResultsCollector.create(page, testInfo.project.use);
9+
collector.withUserPreferences({
10+
messageSecret: 'ABC',
11+
javascriptInterface: 'javascriptInterface',
12+
messageCallback: 'messageCallback',
13+
});
14+
await collector.load(HTML, CONFIG);
15+
16+
const duckAiDataClearing = new DuckAiDataClearingSpec(page);
17+
18+
// Setup test data
19+
await duckAiDataClearing.setupTestData();
20+
await duckAiDataClearing.waitForDataSetup();
21+
22+
// Trigger data clearing via messaging
23+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
24+
25+
// Wait for completion message
26+
const messages = await collector.waitForMessage('duckAiClearDataCompleted', 1);
27+
expect(messages).toHaveLength(1);
28+
expect(messages[0].payload.method).toBe('duckAiClearDataCompleted');
29+
30+
// Verify data is actually cleared
31+
await duckAiDataClearing.verifyDataCleared();
32+
await duckAiDataClearing.waitForVerification('Verification complete: All data cleared');
33+
});
34+
35+
test('duck-ai-data-clearing feature handles IndexedDB errors gracefully', async ({ page }, testInfo) => {
36+
const collector = ResultsCollector.create(page, testInfo.project.use);
37+
collector.withUserPreferences({
38+
messageSecret: 'ABC',
39+
javascriptInterface: 'javascriptInterface',
40+
messageCallback: 'messageCallback',
41+
});
42+
await collector.load(HTML, CONFIG);
43+
44+
const duckAiDataClearing = new DuckAiDataClearingSpec(page);
45+
46+
// Setup localStorage data only (no IndexedDB)
47+
await duckAiDataClearing.setupLocalStorageOnly();
48+
49+
// Mock IndexedDB to fail
50+
await page.evaluate(() => {
51+
const originalOpen = window.indexedDB.open;
52+
window.indexedDB.open = function () {
53+
const request = originalOpen.call(window.indexedDB, 'nonexistent');
54+
// Immediately fire the error event
55+
setTimeout(() => {
56+
if (typeof request.onerror === 'function') {
57+
// Create a fake event object
58+
const event = new Event('error');
59+
request.onerror(event);
60+
}
61+
}, 0);
62+
return request;
63+
};
64+
});
65+
66+
// Trigger data clearing
67+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
68+
69+
// Should still get completion message (localStorage should clear successfully)
70+
const messages = await collector.waitForMessage('duckAiClearDataFailed', 1);
71+
expect(messages).toHaveLength(1);
72+
expect(messages[0].payload.method).toBe('duckAiClearDataFailed');
73+
});
74+
75+
test('duck-ai-data-clearing feature handles localStorage errors gracefully', async ({ page }, testInfo) => {
76+
const collector = ResultsCollector.create(page, testInfo.project.use);
77+
collector.withUserPreferences({
78+
messageSecret: 'ABC',
79+
javascriptInterface: 'javascriptInterface',
80+
messageCallback: 'messageCallback',
81+
});
82+
await collector.load(HTML, CONFIG);
83+
84+
// Mock localStorage to throw an error
85+
await page.evaluate(() => {
86+
Storage.prototype.removeItem = () => {
87+
throw new Error('Simulated localStorage error');
88+
};
89+
});
90+
91+
// Trigger data clearing
92+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
93+
94+
// Should get failure message
95+
const messages = await collector.waitForMessage('duckAiClearDataFailed', 1);
96+
expect(messages).toHaveLength(1);
97+
expect(messages[0].payload.method).toBe('duckAiClearDataFailed');
98+
});
99+
100+
test('duck-ai-data-clearing feature succeeds when data collections do not exist or are empty', async ({ page }, testInfo) => {
101+
const collector = ResultsCollector.create(page, testInfo.project.use);
102+
collector.withUserPreferences({
103+
messageSecret: 'ABC',
104+
javascriptInterface: 'javascriptInterface',
105+
messageCallback: 'messageCallback',
106+
});
107+
await collector.load(HTML, CONFIG);
108+
109+
const duckAiDataClearing = new DuckAiDataClearingSpec(page);
110+
111+
// Ensure localStorage item doesn't exist
112+
await page.evaluate(() => {
113+
localStorage.removeItem('savedAIChats');
114+
});
115+
116+
// Ensure IndexedDB is clean (no existing database or object store)
117+
await page.evaluate(() => {
118+
return new Promise((resolve) => {
119+
const deleteRequest = indexedDB.deleteDatabase('savedAIChatData');
120+
deleteRequest.onsuccess = () => resolve(null);
121+
deleteRequest.onerror = () => resolve(null); // Continue even if delete fails
122+
deleteRequest.onblocked = () => resolve(null); // Continue even if blocked
123+
});
124+
});
125+
126+
// Trigger data clearing on non-existent/empty data
127+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
128+
129+
// Should still get completion message since there's nothing to fail
130+
const messages = await collector.waitForMessage('duckAiClearDataCompleted', 1);
131+
expect(messages).toHaveLength(1);
132+
expect(messages[0].payload.method).toBe('duckAiClearDataCompleted');
133+
134+
// Verify that subsequent verification shows no data exists
135+
await duckAiDataClearing.verifyDataCleared();
136+
await duckAiDataClearing.waitForVerification('Verification complete: All data cleared');
137+
});
138+
139+
class DuckAiDataClearingSpec {
140+
/**
141+
* @param {import("@playwright/test").Page} page
142+
*/
143+
constructor(page) {
144+
this.page = page;
145+
}
146+
147+
async setupTestData() {
148+
await this.page.click('#setup-data');
149+
}
150+
151+
async setupLocalStorageOnly() {
152+
await this.page.evaluate(() => {
153+
localStorage.setItem('savedAIChats', JSON.stringify([{ id: 1, message: 'test chat 1' }]));
154+
});
155+
}
156+
157+
async waitForDataSetup() {
158+
await this.page.waitForFunction(
159+
() => {
160+
const status = document.getElementById('data-status');
161+
return status && status.textContent === 'Test data setup complete';
162+
},
163+
{ timeout: 5000 },
164+
);
165+
}
166+
167+
async verifyDataCleared() {
168+
await this.page.click('#verify-data');
169+
}
170+
171+
async waitForVerification(expectedText) {
172+
await this.page.waitForFunction(
173+
(expected) => {
174+
const status = document.getElementById('verify-status');
175+
return status && status.textContent === expected;
176+
},
177+
expectedText,
178+
{ timeout: 5000 },
179+
);
180+
}
181+
}
182+
183+
export { DuckAiDataClearingSpec };
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"readme": "This config is used to test enabling the Duck.ai data clearing feature.",
3+
"version": 1,
4+
"features": {
5+
"duckAiDataClearing": {
6+
"state": "enabled",
7+
"settings": {
8+
"chatsLocalStorageKeys": ["savedAIChats"],
9+
"chatImagesIndexDbNameObjectStoreNamePairs": [["savedAIChatData", "chat-images"]]
10+
},
11+
"exceptions": []
12+
}
13+
},
14+
"unprotectedTemporary": []
15+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Duck AI Data Clearing Test</title>
7+
<link rel="stylesheet" href="../shared/style.css">
8+
</head>
9+
<body>
10+
<header>
11+
<h1>Duck AI Data Clearing Test</h1>
12+
<p>This page tests the Duck AI data clearing functionality.</p>
13+
</header>
14+
15+
<main>
16+
<section>
17+
<h2>Setup Data</h2>
18+
<button id="setup-data">Setup Test Data</button>
19+
<div id="data-status"></div>
20+
</section>
21+
22+
<section>
23+
<h2>Clear Data</h2>
24+
<button id="clear-data">Clear Duck AI Data</button>
25+
<div id="clear-status"></div>
26+
</section>
27+
28+
<section>
29+
<h2>Verify Data Cleared</h2>
30+
<button id="verify-data">Verify Data Cleared</button>
31+
<div id="verify-status"></div>
32+
</section>
33+
</main>
34+
35+
<script src="../shared/utils.js"></script>
36+
<script>
37+
const dataStatus = document.getElementById('data-status');
38+
const clearStatus = document.getElementById('clear-status');
39+
const verifyStatus = document.getElementById('verify-status');
40+
41+
// Setup test data
42+
document.getElementById('setup-data').addEventListener('click', async () => {
43+
try {
44+
// Setup localStorage data
45+
localStorage.setItem('savedAIChats', JSON.stringify([
46+
{ id: 1, message: 'test chat 1' },
47+
{ id: 2, message: 'test chat 2' }
48+
]));
49+
50+
// Setup IndexedDB data
51+
const request = indexedDB.open('savedAIChatData', 1);
52+
53+
request.onupgradeneeded = (event) => {
54+
const db = event.target.result;
55+
if (!db.objectStoreNames.contains('chat-images')) {
56+
const objectStore = db.createObjectStore('chat-images', { keyPath: 'id' });
57+
}
58+
};
59+
60+
request.onsuccess = (event) => {
61+
const db = event.target.result;
62+
const transaction = db.transaction(['chat-images'], 'readwrite');
63+
const objectStore = transaction.objectStore('chat-images');
64+
65+
objectStore.add({ id: 1, image: 'test-image-1' });
66+
objectStore.add({ id: 2, image: 'test-image-2' });
67+
68+
transaction.oncomplete = () => {
69+
dataStatus.textContent = 'Test data setup complete';
70+
db.close();
71+
};
72+
};
73+
74+
request.onerror = () => {
75+
dataStatus.textContent = 'Error setting up test data';
76+
};
77+
} catch (error) {
78+
dataStatus.textContent = 'Error: ' + error.message;
79+
}
80+
});
81+
82+
// Verify data is cleared
83+
document.getElementById('verify-data').addEventListener('click', async () => {
84+
try {
85+
// Check localStorage
86+
const localStorageData = localStorage.getItem('savedAIChats');
87+
const localStorageCleared = localStorageData === null;
88+
89+
// Check IndexedDB
90+
const request = indexedDB.open('savedAIChatData');
91+
92+
request.onsuccess = (event) => {
93+
const db = event.target.result;
94+
95+
if (!db.objectStoreNames.contains('chat-images')) {
96+
verifyStatus.textContent = 'Verification complete: All data cleared';
97+
db.close();
98+
return;
99+
}
100+
101+
const transaction = db.transaction(['chat-images'], 'readonly');
102+
const objectStore = transaction.objectStore('chat-images');
103+
const countRequest = objectStore.count();
104+
105+
countRequest.onsuccess = () => {
106+
const count = countRequest.result;
107+
const indexedDBCleared = count === 0;
108+
109+
if (localStorageCleared && indexedDBCleared) {
110+
verifyStatus.textContent = 'Verification complete: All data cleared';
111+
} else {
112+
verifyStatus.textContent = `Verification failed: localStorage cleared: ${localStorageCleared}, IndexedDB cleared: ${indexedDBCleared}`;
113+
}
114+
db.close();
115+
};
116+
};
117+
118+
request.onerror = () => {
119+
verifyStatus.textContent = 'Error verifying data clearing';
120+
};
121+
} catch (error) {
122+
verifyStatus.textContent = 'Error: ' + error.message;
123+
}
124+
});
125+
</script>
126+
</body>
127+
</html>

injected/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
"@types/chrome": "^0.1.1",
4141
"@types/jasmine": "^5.1.9",
4242
"@types/node": "^24.1.0",
43-
"@typescript-eslint/eslint-plugin": "^8.44.1",
43+
"@typescript-eslint/eslint-plugin": "^8.45.0",
4444
"fast-check": "^4.2.0",
45-
"jasmine": "^5.11.0",
45+
"jasmine": "^5.12.0",
4646
"web-ext": "^8.10.0"
4747
}
4848
}

injected/playwright.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default defineConfig({
1212
'integration-test/windows-permissions.spec.js',
1313
'integration-test/broker-protection-tests/**/*.spec.js',
1414
'integration-test/breakage-reporting.spec.js',
15+
'integration-test/duck-ai-data-clearing.spec.js',
1516
],
1617
use: { injectName: 'windows', platform: 'windows' },
1718
},

injected/src/features.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const otherFeatures = /** @type {const} */ ([
2222
'duckPlayer',
2323
'duckPlayerNative',
2424
'duckAiListener',
25+
'duckAiDataClearing',
2526
'harmfulApis',
2627
'webCompat',
2728
'windowsPermissionUsage',
@@ -37,11 +38,12 @@ const otherFeatures = /** @type {const} */ ([
3738
/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
3839
/** @type {Record<string, FeatureName[]>} */
3940
export const platformSupport = {
40-
apple: ['webCompat', 'duckPlayerNative', ...baseFeatures, 'duckAiListener', 'pageContext'],
41+
apple: ['webCompat', 'duckPlayerNative', ...baseFeatures, 'duckAiListener', 'duckAiDataClearing', 'pageContext'],
4142
'apple-isolated': [
4243
'duckPlayer',
4344
'duckPlayerNative',
4445
'brokerProtection',
46+
'breakageReporting',
4547
'performanceMetrics',
4648
'clickToLoad',
4749
'messageBridge',
@@ -73,6 +75,7 @@ export const platformSupport = {
7375
'webCompat',
7476
'pageContext',
7577
'duckAiListener',
78+
'duckAiDataClearing',
7679
],
7780
firefox: ['cookie', ...baseFeatures, 'clickToLoad'],
7881
chrome: ['cookie', ...baseFeatures, 'clickToLoad'],

injected/src/features/autofill-import.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ export default class AutofillImport extends ActionExecutorBase {
477477
}
478478
}
479479
} catch {
480-
console.error('password-import: failed for path:', pathname);
480+
this.log.error('password-import: failed for path:', pathname);
481481
}
482482
}
483483
}

injected/src/features/broker-protection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ActionExecutorBase extends ContentFeature {
4040
return this.messaging.notify('actionError', { error: 'No response found, exceptions: ' + exceptions.join(', ') });
4141
}
4242
} catch (e) {
43-
console.log('unhandled exception: ', e);
43+
this.log.error('unhandled exception: ', e);
4444
return this.messaging.notify('actionError', { error: e.toString() });
4545
}
4646
}

0 commit comments

Comments
 (0)