jam-cloud/web/spec/playwright/quick-start-private-session...

198 lines
7.1 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const APP_ORIGIN = process.env.FRONTEND_URL || 'http://www.jamkazam.test:3000';
const APP_HOST = new URL(APP_ORIGIN).hostname;
// Default from active local logs; can be overridden at runtime:
// REMEMBER_TOKEN=... npx playwright test ...
const REMEMBER_TOKEN = process.env.REMEMBER_TOKEN || 'xAkhA3BiUZTjTM7hovMP_g';
test.describe('Quick Start Private Session', () => {
test('enters session and stays there (no immediate auto-leave)', async ({ page, context }) => {
const jsErrors = [];
const consoleErrors = [];
const participantDeletes = [];
page.on('pageerror', (err) => {
jsErrors.push(String(err));
});
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
page.on('request', (request) => {
const method = request.method();
const url = request.url();
if (method === 'DELETE' && /\/api\/participants\//.test(url)) {
const event = { method, url, ts: new Date().toISOString() };
participantDeletes.push(event);
page.evaluate((e) => {
if (!window.__jkParticipantDeleteRequests) window.__jkParticipantDeleteRequests = [];
window.__jkParticipantDeleteRequests.push(e);
}, event).catch(() => {});
}
});
// Must exist before first request so server computes gon.isNativeClient=true.
await context.addCookies([
{
name: 'remember_token',
value: REMEMBER_TOKEN,
domain: APP_HOST,
path: '/',
httpOnly: false,
secure: false,
},
{
name: 'act_as_native_client',
value: 'true',
domain: APP_HOST,
path: '/',
httpOnly: false,
secure: false,
},
]);
await context.addInitScript(() => {
try {
window.localStorage.setItem('jk.webClient.webrtc', '1');
} catch (e) {
// ignore
}
// Test-only instrumentation: capture leaveSession callers with stack.
window.__jkLeaveActionTraces = [];
window.__jkParticipantDeleteRequests = [];
const patchLeaveAction = () => {
const actions = window.SessionActions;
if (!actions || !actions.leaveSession) return;
const action = actions.leaveSession;
if (action.__jkPlaywrightPatched) return;
const pushTrace = (source, argsLike) => {
const args = Array.prototype.slice.call(argsLike || []);
window.__jkLeaveActionTraces.push({
ts: new Date().toISOString(),
source,
args,
stack: (new Error('playwright leaveSession trace')).stack,
});
};
const wrapped = function wrappedLeaveAction(...args) {
pushTrace('call', args);
return action.apply(this, args);
};
Object.assign(wrapped, action);
if (typeof action.trigger === 'function') {
const originalTrigger = action.trigger.bind(action);
wrapped.trigger = (...args) => {
pushTrace('trigger', args);
return originalTrigger(...args);
};
}
wrapped.__jkPlaywrightPatched = true;
actions.leaveSession = wrapped;
};
setInterval(patchLeaveAction, 100);
});
let joinedUrl = null;
try {
await page.goto('/client#', { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => {
return !!(window.gon && window.gon.isNativeClient);
}, { timeout: 20000 });
await expect(page.locator('h2', { hasText: /create session/i })).toBeVisible({ timeout: 20000 });
await page.locator('.createsession').first().click();
const quickStartSolo = page.locator('.quick-start-solo', { hasText: /quick start private/i });
await expect(quickStartSolo).toBeVisible({ timeout: 20000 });
await quickStartSolo.click();
await page.waitForURL(/\/client#\/session\/[0-9a-f-]+/i, { timeout: 30000 });
joinedUrl = page.url();
const myTrack = page.locator('#session-screen .session-my-tracks .session-track.my-track');
await expect(myTrack).toBeVisible({ timeout: 15000 });
// Guard against immediate auto-leave (the current regression).
await page.waitForTimeout(7000);
const leaveTraces = await page.evaluate(() => window.__jkLeaveActionTraces || []);
await expect(page).toHaveURL(/\/client#\/session\/[0-9a-f-]+/i);
expect(page.url(), 'URL changed after join').toBe(joinedUrl);
expect(
participantDeletes,
`Unexpected DELETE /api/participants requests: ${JSON.stringify(participantDeletes, null, 2)}\nleave traces=${JSON.stringify(leaveTraces, null, 2)}`
).toHaveLength(0);
expect(jsErrors, `Page JS errors: ${JSON.stringify(jsErrors, null, 2)}`).toEqual([]);
} finally {
const diagnostics = await page.evaluate(() => {
const collector = window.JK && window.JK.DebugLogCollector;
return {
leaveActionTraces: window.__jkLeaveActionTraces || [],
joinAborts: window.__jkJoinAborts || [],
participantDeleteRequests: window.__jkParticipantDeleteRequests || [],
debugCollectorBuffer: (collector && collector.getBuffer && collector.getBuffer()) || [],
url: window.location.href,
};
}).catch(() => ({
leaveActionTraces: [],
joinAborts: [],
participantDeleteRequests: [],
debugCollectorBuffer: [],
url: page.url(),
}));
await test.info().attach('leave-traces.json', {
body: JSON.stringify(diagnostics.leaveActionTraces, null, 2),
contentType: 'application/json',
});
await test.info().attach('participant-delete-requests.json', {
body: JSON.stringify(diagnostics.participantDeleteRequests, null, 2),
contentType: 'application/json',
});
await test.info().attach('join-aborts.json', {
body: JSON.stringify(diagnostics.joinAborts, null, 2),
contentType: 'application/json',
});
await test.info().attach('debug-log-collector-buffer.json', {
body: JSON.stringify(diagnostics.debugCollectorBuffer, null, 2),
contentType: 'application/json',
});
await test.info().attach('browser-errors.json', {
body: JSON.stringify({ jsErrors, consoleErrors, participantDeletes, joinedUrl, finalUrl: diagnostics.url }, null, 2),
contentType: 'application/json',
});
// Emit to stdout for quick local triage when attachments are inconvenient to inspect.
// eslint-disable-next-line no-console
console.log('PW_DIAGNOSTICS_START');
// eslint-disable-next-line no-console
console.log(JSON.stringify({
leaveActionTraces: diagnostics.leaveActionTraces,
joinAborts: diagnostics.joinAborts,
participantDeleteRequests: diagnostics.participantDeleteRequests,
debugCollectorTail: diagnostics.debugCollectorBuffer.slice(-40),
jsErrors,
consoleErrors,
participantDeletes,
joinedUrl,
finalUrl: diagnostics.url,
}));
// eslint-disable-next-line no-console
console.log('PW_DIAGNOSTICS_END');
}
});
});