azon_seeker/scripts/verify-extension-sync-e2e.mjs

116 lines
3.8 KiB
JavaScript

const debugPort = process.env.CHROMIUM_DEBUG_PORT || '9226';
const syncId = process.env.AZON_E2E_SYNC_ID;
const expectFailure = process.env.AZON_EXPECT_SYNC_FAILURE === '1';
if (!syncId) throw new Error('AZON_E2E_SYNC_ID is required.');
let worker;
for (let attempt = 0; attempt < 60 && !worker; attempt++) {
const targets = await fetch(`http://127.0.0.1:${debugPort}/json/list`).then((response) =>
response.json(),
);
worker = targets.find(
(target) =>
target.type === 'service_worker' && target.url.endsWith('/dist/background/index.mjs'),
);
if (!worker) await new Promise((resolve) => setTimeout(resolve, 250));
}
if (!worker) throw new Error('Azon Seeker background service worker was not found.');
const socket = new WebSocket(worker.webSocketDebuggerUrl);
const pending = new Map();
let nextId = 1;
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (!message.id || !pending.has(message.id)) return;
const { resolve, reject } = pending.get(message.id);
pending.delete(message.id);
message.error ? reject(new Error(message.error.message)) : resolve(message.result);
});
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true });
socket.addEventListener('error', reject, { once: true });
});
function request(method, params = {}) {
return new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
socket.send(JSON.stringify({ id, method, params }));
});
}
async function evaluate(expression) {
const result = await request('Runtime.evaluate', {
expression,
awaitPromise: true,
returnByValue: true,
});
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text);
return result.result.value;
}
const searchItem = {
syncId,
asin: 'B000000098',
keywords: 'extension queue e2e',
rank: 1,
page: 1,
pageRank: 1,
link: 'https://www.amazon.com/dp/B000000098',
title: 'Extension queue end-to-end fixture',
price: '$33.93',
strikethroughPrice: '$39.99',
adType: 'sponsoredDisplay',
adRank: 1,
placement: 'sidebar',
placementRank: 1,
placementId: 'extension-e2e-placement',
adProductIndex: 1,
adFormat: 'standard',
imageSrc: 'https://example.com/extension-e2e.jpg',
createTime: '2026/08/31 11:45:00',
};
await evaluate(`(async () => {
await chrome.storage.local.set({
'amazon-sync-queue-v1': ${JSON.stringify({
searchItems: [searchItem],
detailItems: [],
reviews: [],
})},
'amazon-sync-state-v1': JSON.stringify({status: 'pending', pendingCount: 1})
});
await chrome.alarms.create('amazon-sync-retry', { when: Date.now() + 200 });
return true;
})()`);
let state;
for (let attempt = 0; attempt < 30; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 500));
state = await evaluate(`(async () => {
const stored = await chrome.storage.local.get(['amazon-sync-queue-v1', 'amazon-sync-state-v1']);
return {
queue: stored['amazon-sync-queue-v1'],
state: JSON.parse(stored['amazon-sync-state-v1'])
};
})()`);
if (state.state.status === 'failed') {
if (expectFailure) break;
throw new Error(state.state.lastError);
}
if (state.state.status === 'synced' && state.state.pendingCount === 0) break;
}
socket.close();
if (expectFailure) {
if (
state.state.status !== 'failed' ||
state.state.pendingCount !== 1 ||
state.queue.searchItems.length !== 1
) {
throw new Error(`Failed synchronization was not retained: ${JSON.stringify(state)}`);
}
console.log(JSON.stringify({ syncId, retained: true, ...state.state }, null, 2));
process.exit(0);
}
if (state.state.status !== 'synced' || state.queue.searchItems.length !== 0) {
throw new Error(`Queue did not drain: ${JSON.stringify(state)}`);
}
console.log(JSON.stringify({ syncId, ...state.state }, null, 2));