Skip to content

Commit c613797

Browse files
committed
Implement Duck.ai data clearing feature
1 parent 8da9e21 commit c613797

File tree

6 files changed

+358
-2
lines changed

6 files changed

+358
-2
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 = () => {
53+
const request = originalOpen.call(window.indexedDB, 'nonexistent');
54+
// Simulate an error
55+
setTimeout(() => {
56+
if (request.onerror) {
57+
request.onerror(new Error('Simulated IndexedDB error'));
58+
}
59+
}, 10);
60+
return request;
61+
};
62+
});
63+
64+
// Trigger data clearing
65+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
66+
67+
// Should still get completion message (localStorage should clear successfully)
68+
const messages = await collector.waitForMessage('duckAiClearDataFailed', 1);
69+
expect(messages).toHaveLength(1);
70+
expect(messages[0].payload.method).toBe('duckAiClearDataFailed');
71+
});
72+
73+
test('duck-ai-data-clearing feature handles localStorage errors gracefully', async ({ page }, testInfo) => {
74+
const collector = ResultsCollector.create(page, testInfo.project.use);
75+
collector.withUserPreferences({
76+
messageSecret: 'ABC',
77+
javascriptInterface: 'javascriptInterface',
78+
messageCallback: 'messageCallback',
79+
});
80+
await collector.load(HTML, CONFIG);
81+
82+
// Mock localStorage to throw an error
83+
await page.evaluate(() => {
84+
const originalRemoveItem = Storage.prototype.removeItem;
85+
Storage.prototype.removeItem = () => {
86+
throw new Error('Simulated localStorage error');
87+
};
88+
});
89+
90+
// Trigger data clearing
91+
await collector.simulateSubscriptionMessage('duckAiDataClearing', 'duckAiClearData', {});
92+
93+
// Should get failure message
94+
const messages = await collector.waitForMessage('duckAiClearDataFailed', 1);
95+
expect(messages).toHaveLength(1);
96+
expect(messages[0].payload.method).toBe('duckAiClearDataFailed');
97+
});
98+
99+
class DuckAiDataClearingSpec {
100+
/**
101+
* @param {import("@playwright/test").Page} page
102+
*/
103+
constructor(page) {
104+
this.page = page;
105+
}
106+
107+
async setupTestData() {
108+
await this.page.click('#setup-data');
109+
}
110+
111+
async setupLocalStorageOnly() {
112+
await this.page.evaluate(() => {
113+
localStorage.setItem('savedAIChats', JSON.stringify([
114+
{ id: 1, message: 'test chat 1' }
115+
]));
116+
});
117+
}
118+
119+
async waitForDataSetup() {
120+
await this.page.waitForFunction(() => {
121+
const status = document.getElementById('data-status');
122+
return status && status.textContent === 'Test data setup complete';
123+
}, { timeout: 5000 });
124+
}
125+
126+
async verifyDataCleared() {
127+
await this.page.click('#verify-data');
128+
}
129+
130+
async waitForVerification(expectedText) {
131+
await this.page.waitForFunction((expected) => {
132+
const status = document.getElementById('verify-status');
133+
return status && status.textContent === expected;
134+
}, expectedText, { timeout: 5000 });
135+
}
136+
}
137+
138+
export { DuckAiDataClearingSpec };
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"features": {
3+
"duckAiDataClearing": {
4+
"state": "enabled"
5+
}
6+
},
7+
"unprotectedTemporary": [],
8+
"featureToggles": {}
9+
}
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/playwright.config.js

Lines changed: 2 additions & 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
},
@@ -51,6 +52,7 @@ export default defineConfig({
5152
'integration-test/duckplayer-mobile-drawer.spec.js',
5253
'integration-test/web-compat-android.spec.js',
5354
'integration-test/message-bridge-android.spec.js',
55+
'integration-test/duck-ai-data-clearing.spec.js',
5456
],
5557
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] },
5658
},

injected/src/features.js

Lines changed: 4 additions & 2 deletions
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,7 +38,7 @@ 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',
@@ -47,7 +48,7 @@ export const platformSupport = {
4748
'messageBridge',
4849
'favicon',
4950
],
50-
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
51+
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge', 'duckAiDataClearing'],
5152
'android-broker-protection': ['brokerProtection'],
5253
'android-autofill-password-import': ['autofillPasswordImport'],
5354
'android-adsjs': [
@@ -73,6 +74,7 @@ export const platformSupport = {
7374
'webCompat',
7475
'pageContext',
7576
'duckAiListener',
77+
'duckAiDataClearing',
7678
],
7779
firefox: ['cookie', ...baseFeatures, 'clickToLoad'],
7880
chrome: ['cookie', ...baseFeatures, 'clickToLoad'],

0 commit comments

Comments
 (0)