Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -263,7 +263,7 @@ async function main(): Promise<number> {
|
|||||||
// ─── Setup: launch browser, attach to SW + open popup bridge ───
|
// ─── Setup: launch browser, attach to SW + open popup bridge ───
|
||||||
process.stdout.write('\nLaunching Chrome + opening popup bridge...\n');
|
process.stdout.write('\nLaunching Chrome + opening popup bridge...\n');
|
||||||
handles = await launchHarnessBrowser();
|
handles = await launchHarnessBrowser();
|
||||||
const { browser, sw, page, popup, extensionId, downloadsDir } = handles;
|
const { browser, sw, page, popup, extension, extensionId, downloadsDir } = handles;
|
||||||
process.stdout.write(` extensionId: ${extensionId}\n`);
|
process.stdout.write(` extensionId: ${extensionId}\n`);
|
||||||
process.stdout.write(` downloadsDir: ${downloadsDir}\n`);
|
process.stdout.write(` downloadsDir: ${downloadsDir}\n`);
|
||||||
process.stdout.write(` popup: chrome-extension://${extensionId}/src/popup/index.html\n\n`);
|
process.stdout.write(` popup: chrome-extension://${extensionId}/src/popup/index.html\n\n`);
|
||||||
@@ -283,16 +283,134 @@ async function main(): Promise<number> {
|
|||||||
const manifest = await getManifest(popupPage);
|
const manifest = await getManifest(popupPage);
|
||||||
const expectedVersion = manifest.version;
|
const expectedVersion = manifest.version;
|
||||||
|
|
||||||
// ─── Wave 3 stubbed assertions (Tasks 4-7 will wire these) ──────
|
// ─── Assertion 1: SW bootstrap → setIdleMode ───────────────────
|
||||||
|
const a1 = await runAssertion(
|
||||||
|
1,
|
||||||
|
'SW bootstrap → setIdleMode (badge="" + popup="" + isRecording=false)',
|
||||||
|
buffers,
|
||||||
|
async () => {
|
||||||
|
// Wake the SW via the keepalive ping first — this ensures the
|
||||||
|
// SW's initialize() has been observed before we sample state.
|
||||||
|
// In MV3, the SW may have suspended between launch and now;
|
||||||
|
// sendMessage wakes it for 30s.
|
||||||
|
await keepalivePing(popupPage);
|
||||||
|
// After SW init (production initialize() calls setIdleMode()),
|
||||||
|
// badge becomes empty + popup becomes empty + isRecording is false.
|
||||||
|
// Poll for the popup transition because setIdleMode's setPopup
|
||||||
|
// call is async-propagated through the action API; the popup
|
||||||
|
// page may briefly observe the manifest default before the
|
||||||
|
// override lands.
|
||||||
|
const popupUrl = await waitFor(
|
||||||
|
() => getPopup(popupPage),
|
||||||
|
(v) => v === '',
|
||||||
|
3_000,
|
||||||
|
'popup should become empty after SW initialize() runs setIdleMode',
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
assertEqual(popupUrl, '', 'popup expected empty in idle mode');
|
||||||
|
const badge = await getBadgeText(popupPage);
|
||||||
|
assertEqual(badge, '', 'badge expected empty after SW bootstrap');
|
||||||
|
const recording = await getIsRecording(popupPage);
|
||||||
|
assertEqual(recording, false, 'isRecording expected false at bootstrap');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
results.push(a1);
|
||||||
|
|
||||||
|
// ─── Assertion 2: Toolbar onClicked-idle → REC + popup ─────────
|
||||||
|
const a2 = await runAssertion(
|
||||||
|
2,
|
||||||
|
'toolbar onClicked-idle → badge "REC" + popup set + isRecording=true',
|
||||||
|
buffers,
|
||||||
|
async () => {
|
||||||
|
// Trigger the toolbar action — the production code's
|
||||||
|
// chrome.action.onClicked listener calls startVideoCapture(),
|
||||||
|
// which creates the offscreen + starts recording + transitions
|
||||||
|
// the badge to REC + sets the popup to popup/index.html.
|
||||||
|
await page.bringToFront();
|
||||||
|
await page.triggerExtensionAction(extension);
|
||||||
|
// Badge transition is async (offscreen create + handshake +
|
||||||
|
// getDisplayMedia picker + post-grant validation + setBadgeText).
|
||||||
|
// Poll up to 8s; the auto-select picker shaves most of the time.
|
||||||
|
const badge = await waitFor(
|
||||||
|
() => getBadgeText(popupPage),
|
||||||
|
(v) => v === 'REC',
|
||||||
|
8_000,
|
||||||
|
'badge should become REC after toolbar click',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
assertEqual(badge, 'REC', 'badge after toolbar click');
|
||||||
|
const popupUrl = await getPopup(popupPage);
|
||||||
|
assertMatch(
|
||||||
|
popupUrl,
|
||||||
|
/src\/popup\/index\.html$/,
|
||||||
|
'popup should be set to popup/index.html during recording',
|
||||||
|
);
|
||||||
|
const recording = await getIsRecording(popupPage);
|
||||||
|
assertEqual(recording, true, 'isRecording expected true after click');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
results.push(a2);
|
||||||
|
|
||||||
|
// ─── Assertion 3: offscreen displaySurface === 'monitor' ───────
|
||||||
|
const a3 = await runAssertion(
|
||||||
|
3,
|
||||||
|
'offscreen getCurrentStream displaySurface === "monitor"',
|
||||||
|
buffers,
|
||||||
|
async () => {
|
||||||
|
// A2 left recording active. The offscreen document was created
|
||||||
|
// by startVideoCapture; wait for the target + attach.
|
||||||
|
const offTarget = await waitForOffscreenTarget(browser, extensionId);
|
||||||
|
const offPage = await attachToOffscreen(offTarget);
|
||||||
|
offPage.on('console', (msg) => {
|
||||||
|
buffers.offscreenLines.push(`[OS:${msg.type()}] ${msg.text()}`);
|
||||||
|
});
|
||||||
|
const ds = await getDisplaySurface(offPage);
|
||||||
|
assertEqual(
|
||||||
|
ds,
|
||||||
|
'monitor',
|
||||||
|
'displaySurface expected "monitor" per post-grant validation (D-15)',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
results.push(a3);
|
||||||
|
|
||||||
|
// ─── Assertion 4: onClicked-recording → popup, no new offscreen
|
||||||
|
const a4 = await runAssertion(
|
||||||
|
4,
|
||||||
|
'toolbar onClicked while recording → popup opens, no new offscreen target',
|
||||||
|
buffers,
|
||||||
|
async () => {
|
||||||
|
const offscreenCountBefore = countOffscreenTargets(browser, extensionId);
|
||||||
|
// Triggering the action during recording opens the popup
|
||||||
|
// (production code keeps popup set in REC mode); the click
|
||||||
|
// does NOT spawn a second offscreen.
|
||||||
|
await page.bringToFront();
|
||||||
|
await page.triggerExtensionAction(extension);
|
||||||
|
// Give the popup a moment to open + Chrome to settle.
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
const offscreenCountAfter = countOffscreenTargets(browser, extensionId);
|
||||||
|
assertEqual(
|
||||||
|
offscreenCountAfter,
|
||||||
|
offscreenCountBefore,
|
||||||
|
'offscreen target count must not increase on second click during recording',
|
||||||
|
);
|
||||||
|
// popup must still be set (recording is ongoing).
|
||||||
|
const popupUrl = await getPopup(popupPage);
|
||||||
|
assertMatch(
|
||||||
|
popupUrl,
|
||||||
|
/src\/popup\/index\.html$/,
|
||||||
|
'popup must remain set while recording',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
results.push(a4);
|
||||||
|
|
||||||
|
// ─── Wave 3 STUBBED assertions (Tasks 5-7 will wire these) ─────
|
||||||
const stubs: Array<{
|
const stubs: Array<{
|
||||||
index: number;
|
index: number;
|
||||||
name: string;
|
name: string;
|
||||||
taskNumber: number;
|
taskNumber: number;
|
||||||
}> = [
|
}> = [
|
||||||
{ index: 1, name: 'SW bootstrap → setIdleMode', taskNumber: 4 },
|
|
||||||
{ index: 2, name: 'toolbar onClicked-idle → badge REC + popup', taskNumber: 4 },
|
|
||||||
{ index: 3, name: 'offscreen displaySurface === monitor', taskNumber: 4 },
|
|
||||||
{ index: 4, name: 'toolbar onClicked-recording → popup, no new offscreen', taskNumber: 4 },
|
|
||||||
{ index: 5, name: 'SAVE_ARCHIVE → download fires + zip appears', taskNumber: 5 },
|
{ index: 5, name: 'SAVE_ARCHIVE → download fires + zip appears', taskNumber: 5 },
|
||||||
{ index: 6, name: 'BUG B canonical: simulateUserStop → badge OFF + no recovery notif', taskNumber: 5 },
|
{ index: 6, name: 'BUG B canonical: simulateUserStop → badge OFF + no recovery notif', taskNumber: 5 },
|
||||||
{ index: 7, name: 'RECORDING_ERROR codec-unsupported → badge ERR + recovery notif', taskNumber: 5 },
|
{ index: 7, name: 'RECORDING_ERROR codec-unsupported → badge ERR + recovery notif', taskNumber: 5 },
|
||||||
@@ -314,30 +432,17 @@ async function main(): Promise<number> {
|
|||||||
results.push(rec);
|
results.push(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress unused-warning placeholders — Tasks 4-7 will use these
|
// Suppress unused-warning placeholders for Tasks 5-7 helpers.
|
||||||
// imports + handles directly. Reference them here for type-clean.
|
|
||||||
void browser;
|
|
||||||
void page;
|
|
||||||
void popupPage;
|
|
||||||
void expectedVersion;
|
void expectedVersion;
|
||||||
void waitForOffscreenTarget;
|
|
||||||
void attachToOffscreen;
|
|
||||||
void countOffscreenTargets;
|
|
||||||
void waitFor;
|
|
||||||
void getBadgeText;
|
|
||||||
void getPopup;
|
|
||||||
void getIsRecording;
|
|
||||||
void getIconSize;
|
void getIconSize;
|
||||||
void fireOnStartup;
|
void fireOnStartup;
|
||||||
void sendSyntheticRecordingError;
|
void sendSyntheticRecordingError;
|
||||||
void getNotificationSnapshot;
|
void getNotificationSnapshot;
|
||||||
void keepalivePing;
|
void keepalivePing;
|
||||||
void getDisplaySurface;
|
|
||||||
void simulateUserStop;
|
void simulateUserStop;
|
||||||
void getSegmentCount;
|
void getSegmentCount;
|
||||||
void assertArchiveShape;
|
void assertArchiveShape;
|
||||||
void extractEntryToFile;
|
void extractEntryToFile;
|
||||||
void assertMatch;
|
|
||||||
void assertTrue;
|
void assertTrue;
|
||||||
void assertGte;
|
void assertGte;
|
||||||
void waitForDownloadedZip;
|
void waitForDownloadedZip;
|
||||||
|
|||||||
@@ -245,15 +245,21 @@ export async function getNotificationSnapshot(
|
|||||||
/**
|
/**
|
||||||
* Send a no-op keepalive ping to the SW so Chrome's ~30s idle timer
|
* Send a no-op keepalive ping to the SW so Chrome's ~30s idle timer
|
||||||
* does not evict the worker during long waits (assertion 11's 35s
|
* does not evict the worker during long waits (assertion 11's 35s
|
||||||
* recording window). Uses chrome.runtime.sendMessage as the cheapest
|
* recording window). Uses a __mokoshTestQuery 'snapshot' op as the
|
||||||
* wake-up signal; the SW's onMessage handler treats unknown messages
|
* cheapest round-trip with a guaranteed response — the bridge handler
|
||||||
* as a warning-log no-op.
|
* answers synchronously with the current notification state.
|
||||||
*
|
*
|
||||||
* @param popup - The extension popup page handle.
|
* @param popup - The extension popup page handle.
|
||||||
*/
|
*/
|
||||||
export async function keepalivePing(popup: Page): Promise<void> {
|
export async function keepalivePing(popup: Page): Promise<void> {
|
||||||
await popup.evaluate(async () => {
|
await popup.evaluate(async () => {
|
||||||
await chrome.runtime.sendMessage({ type: '__mokoshKeepalive' });
|
const msg = { type: '__mokoshTestQuery', op: 'snapshot' };
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
chrome.runtime.sendMessage(msg, () => resolve());
|
||||||
|
// Cap at 2s — if the bridge is gone (SW reload race etc.) we
|
||||||
|
// still continue (the assertion may catch downstream issues).
|
||||||
|
setTimeout(() => resolve(), 2_000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user