Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions src/browser-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import { nativeCapabilities, normalizeNative } from './native.js';
import { createObserver } from './observer.js';

let problem = (status, message) => Object.assign(new Error(message), { status });
let contextFields = Object.freeze(['executionId', 'documentId', 'revision', 'width', 'height']);
let sameContext = (left, right) =>
left &&
right &&
['executionId', 'documentId', 'revision', 'width', 'height'].every(
(key) => left[key] === right[key],
);
left && right && contextFields.every((key) => left[key] === right[key]);
let commands = [
'pause',
'continue',
Expand Down Expand Up @@ -207,24 +204,28 @@ export function createBrowserControl({ artifactsDir, workerId = '0', onReplaced
throw problem(409, 'This managed browser has moved to another run.');
return connection;
};
let discard = async (record, includeCommitted = false) => {
let discard = async (
record,
includeCommitted = false,
reason = problem(409, 'Screenshot context ended.'),
) => {
if (record.status === 'accepted') return;
record.controller.abort(problem(409, 'Screenshot context ended.'));
record.controller.abort(reason);
if (record.status !== 'committed' || includeCommitted) {
records.delete(record.id);
await rm(record.partial, { force: true });
await rm(record.file, { force: true });
}
};
let invalidate = (runId, endingRun = false) => {
let invalidate = (runId, endingRun = false, reason) => {
contexts.delete(runId);
for (let request of nativeRequests.values())
if (request.runId === runId && (endingRun || request.kind !== 'key'))
request.controller.abort(problem(409, 'The native input context ended.'));
for (let record of records.values()) {
if (record.runId !== runId || record.status === 'accepted') continue;
if (record.status === 'committed' && !endingRun) continue;
void track(discard(record, endingRun)).catch(() => {});
void track(discard(record, endingRun, reason)).catch(() => {});
}
};
let active = (record) =>
Expand Down Expand Up @@ -468,7 +469,21 @@ export function createBrowserControl({ artifactsDir, workerId = '0', onReplaced
assignment.id === input.context.executionId && assignment.status !== 'released' && !run.tests[assignment.index].skipReason)))
throw problem(409, 'This execution does not own an active test assignment.');
let previous = contexts.get(input.runId);
if (!sameContext(previous, input.context)) invalidate(input.runId);
if (!sameContext(previous, input.context)) {
let changes = contextFields
.filter((key) => previous?.[key] !== input.context?.[key])
.map((key) => `${key}: ${previous?.[key] ?? 'none'} → ${input.context?.[key] ?? 'none'}`);
let describe = (viewport) =>
`${viewport?.width} × ${viewport?.height} at DPR ${viewport?.pixelRatio}`;
let viewportChange = input.viewportChange
? ` Browser viewport: ${describe(input.viewportChange.previous)} → ${describe(input.viewportChange.measured)}.`
: '';
invalidate(
input.runId,
false,
problem(409, `Screenshot context changed (${changes.join(', ')}).${viewportChange}`),
);
}
if (input.context) {
if (
!validViewport(input.context, Number.MAX_SAFE_INTEGER) ||
Expand Down
6 changes: 5 additions & 1 deletion src/browser-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ export async function createBrowserPool(plan, { size = 1, signal, onExit = () =>
catch { /* Channel closure still rejects and removes this request below. */ }
}
requests.delete(id);
request.reject(new Error(`Browser ${method} cancelled or exceeded its deadline.`, { cause: deadline.reason }));
let reason = deadline.reason;
let message = reason?.name === 'TimeoutError'
? `Browser ${method} exceeded its deadline.`
: `Browser ${method} cancelled: ${reason?.message ?? String(reason)}`;
request.reject(new Error(message.replaceAll(server.wsEndpoint(), '[private browser connection]'), { cause: reason }));
};
deadline.addEventListener('abort', abort, { once: true });
try {
Expand Down
10 changes: 7 additions & 3 deletions src/browser/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,11 @@ let context = (current) =>
...state.viewport,
}
: null;
let syncContext = (current) => {
let syncContext = (current, viewportChange) => {
let value = context(current);
contextSync = contextSync.catch(() => {}).then(() => post('context', { context: value }));
contextSync = contextSync.catch(() => {}).then(() =>
post('context', { context: value, ...(viewportChange ? { viewportChange } : {}) }),
);
return contextSync;
};
let assertCurrent = (current, documentId, revision, metrics, operation = 'screenshot') => {
Expand Down Expand Up @@ -418,9 +420,11 @@ window.addEventListener('message', (event) => {
if (message.kind === 'resize' && message.executionId === resource?.id) {
let measured = { ...appSize(), pixelRatio: appWindow.devicePixelRatio };
if (Object.keys(measured).every((key) => measured[key] === observedViewport[key])) return;
let previous = observedViewport;
observedViewport = measured;
update({ type: 'browser.resized', viewport: appSize() });
if (runId) void syncContext(resource).catch((error) => console.error(error));
if (runId)
void syncContext(resource, { previous, measured }).catch((error) => console.error(error));
}
});
window.addEventListener('pagehide', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/native/pool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ test('cancelling queued viewport work closes the pool before it can be reused',
let cancel = new AbortController();
let resizing = workers[0].resize({ width: 400, height: 300 }, cancel.signal);
cancel.abort(new Error('Viewport request ended'));
await assert.rejects(resizing, /cancelled/);
await assert.rejects(resizing, /cancelled: Viewport request ended/);
await assert.rejects(opening);
await assert.rejects(workers[1].open(origin), /closed/);
await assert.rejects(pool.createWorker('c'), /closed/);
Expand Down
Loading