chore: explicitly reset apiZone instead of everything (#34265)

This commit is contained in:
Dmitry Gozman 2025-01-10 20:15:05 +00:00 committed by GitHub
parent 1f2eb499d2
commit f0a3a15e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 34 deletions

View file

@ -138,7 +138,7 @@ export class Connection extends EventEmitter {
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
// We need to exit zones before calling into the server, otherwise // We need to exit zones before calling into the server, otherwise
// when we receive events from the server, we would be in an API zone. // when we receive events from the server, we would be in an API zone.
zones.exitZones(() => this.onmessage({ ...message, metadata })); zones.empty().run(() => this.onmessage({ ...message, metadata }));
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method }));
} }

View file

@ -820,7 +820,7 @@ export class RouteHandler {
this._times = times; this._times = times;
this.url = url; this.url = url;
this.handler = handler; this.handler = handler;
this._svedZone = zones.currentZone(); this._svedZone = zones.current().without('apiZone');
} }
static prepareInterceptionPatterns(handlers: RouteHandler[]) { static prepareInterceptionPatterns(handlers: RouteHandler[]) {

View file

@ -35,7 +35,7 @@ export class Waiter {
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) { constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
this._waitId = createGuid(); this._waitId = createGuid();
this._channelOwner = channelOwner; this._channelOwner = channelOwner;
this._savedZone = zones.currentZone(); this._savedZone = zones.current().without('apiZone');
this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {}); this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
this._dispose = [ this._dispose = [

View file

@ -19,54 +19,54 @@ import { AsyncLocalStorage } from 'async_hooks';
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone'; export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
class ZoneManager { class ZoneManager {
private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone|undefined>(); private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();
private readonly _emptyZone = Zone.createEmpty(this._asyncLocalStorage);
run<T, R>(type: ZoneType, data: T, func: () => R): R { run<T, R>(type: ZoneType, data: T, func: () => R): R {
const zone = Zone._createWithData(this._asyncLocalStorage, type, data); return this.current().with(type, data).run(func);
return this._asyncLocalStorage.run(zone, func);
} }
zoneData<T>(type: ZoneType): T | undefined { zoneData<T>(type: ZoneType): T | undefined {
const zone = this._asyncLocalStorage.getStore(); return this.current().data(type);
return zone?.get(type);
} }
currentZone(): Zone { current(): Zone {
return this._asyncLocalStorage.getStore() ?? Zone._createEmpty(this._asyncLocalStorage); return this._asyncLocalStorage.getStore() ?? this._emptyZone;
} }
exitZones<R>(func: () => R): R { empty(): Zone {
return this._asyncLocalStorage.run(undefined, func); return this._emptyZone;
} }
} }
export class Zone { export class Zone {
private readonly _asyncLocalStorage: AsyncLocalStorage<Zone | undefined>; private readonly _asyncLocalStorage: AsyncLocalStorage<Zone | undefined>;
private readonly _data: Map<ZoneType, unknown>; private readonly _data: ReadonlyMap<ZoneType, unknown>;
static _createWithData(asyncLocalStorage: AsyncLocalStorage<Zone|undefined>, type: ZoneType, data: unknown) { static createEmpty(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>) {
const store = new Map(asyncLocalStorage.getStore()?._data);
store.set(type, data);
return new Zone(asyncLocalStorage, store);
}
static _createEmpty(asyncLocalStorage: AsyncLocalStorage<Zone|undefined>) {
return new Zone(asyncLocalStorage, new Map()); return new Zone(asyncLocalStorage, new Map());
} }
private constructor(asyncLocalStorage: AsyncLocalStorage<Zone|undefined>, store: Map<ZoneType, unknown>) { private constructor(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>, store: Map<ZoneType, unknown>) {
this._asyncLocalStorage = asyncLocalStorage; this._asyncLocalStorage = asyncLocalStorage;
this._data = store; this._data = store;
} }
run<R>(func: () => R): R { with(type: ZoneType, data: unknown): Zone {
// Reset apiZone and expectZone, but restore stepZone. return new Zone(this._asyncLocalStorage, new Map(this._data).set(type, data));
const entries = [...this._data.entries()].filter(([type]) => (type !== 'apiZone' && type !== 'expectZone'));
const resetZone = new Zone(this._asyncLocalStorage, new Map(entries));
return this._asyncLocalStorage.run(resetZone, func);
} }
get<T>(type: ZoneType): T | undefined { without(type?: ZoneType): Zone {
const data = type ? new Map(this._data) : new Map();
data.delete(type);
return new Zone(this._asyncLocalStorage, data);
}
run<R>(func: () => R): R {
return this._asyncLocalStorage.run(this, func);
}
data<T>(type: ZoneType): T | undefined {
return this._data.get(type) as T | undefined; return this._data.get(type) as T | undefined;
} }
} }

View file

@ -1376,8 +1376,9 @@ test('calls from waitForEvent callback should be under its parent step', {
await page.setContent('<div onclick="fetch(\\'/simple.json\\').then(r => r.text());">Go!</div>'); await page.setContent('<div onclick="fetch(\\'/simple.json\\').then(r => r.text());">Go!</div>');
const responseJson = await test.step('custom step', async () => { const responseJson = await test.step('custom step', async () => {
const responsePromise = page.waitForResponse(async response => { const responsePromise = page.waitForResponse(async response => {
const text = await response.text(); await page.content();
expect(text).toBeTruthy(); await page.content(); // second time a charm!
await expect(page.locator('div')).toContainText('Go');
return true; return true;
}); });
@ -1405,9 +1406,11 @@ pw:api |page.goto(${server.EMPTY_PAGE}) @ a.test.ts:4
pw:api |page.setContent @ a.test.ts:5 pw:api |page.setContent @ a.test.ts:5
test.step |custom step @ a.test.ts:6 test.step |custom step @ a.test.ts:6
pw:api | page.waitForResponse @ a.test.ts:7 pw:api | page.waitForResponse @ a.test.ts:7
pw:api | page.click(div) @ a.test.ts:13 pw:api | page.click(div) @ a.test.ts:14
expect | expect.toBeTruthy @ a.test.ts:9 pw:api | page.content @ a.test.ts:8
expect |expect.toBe @ a.test.ts:17 pw:api | page.content @ a.test.ts:9
expect | expect.toContainText @ a.test.ts:10
expect |expect.toBe @ a.test.ts:18
hook |After Hooks hook |After Hooks
fixture | fixture: page fixture | fixture: page
fixture | fixture: context fixture | fixture: context
@ -1464,7 +1467,8 @@ test('calls from page.route callback should be under its parent step', {
const response = await route.fetch(); const response = await route.fetch();
const text = await response.text(); const text = await response.text();
expect(text).toBe(''); expect(text).toBe('');
await route.fulfill({ response }) await response.text(); // second time a charm!
await route.fulfill({ response });
}); });
await page.goto('${server.EMPTY_PAGE}'); await page.goto('${server.EMPTY_PAGE}');
}); });
@ -1485,9 +1489,10 @@ fixture | fixture: page
pw:api | browserContext.newPage pw:api | browserContext.newPage
test.step |custom step @ a.test.ts:4 test.step |custom step @ a.test.ts:4
pw:api | page.route @ a.test.ts:5 pw:api | page.route @ a.test.ts:5
pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:11 pw:api | page.goto(${server.EMPTY_PAGE}) @ a.test.ts:12
pw:api | apiResponse.text @ a.test.ts:7 pw:api | apiResponse.text @ a.test.ts:7
expect | expect.toBe @ a.test.ts:8 expect | expect.toBe @ a.test.ts:8
pw:api | apiResponse.text @ a.test.ts:9
hook |After Hooks hook |After Hooks
fixture | fixture: page fixture | fixture: page
fixture | fixture: context fixture | fixture: context