feat(inspector): render errors (#5459)

This commit is contained in:
Pavel Feldman 2021-02-13 22:13:51 -08:00 committed by GitHub
parent ae2ffb3fb9
commit 8b9a2afd3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 191 additions and 80 deletions

View File

@ -14,5 +14,4 @@
* limitations under the License. * limitations under the License.
*/ */
const { setUnderTest } = require('./lib/utils/utils');
module.exports = require('./lib/inprocess'); module.exports = require('./lib/inprocess');

View File

@ -214,6 +214,7 @@ export class DispatcherConnection {
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) }); this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
} catch (e) { } catch (e) {
// Dispatching error // Dispatching error
callMetadata.error = e.message;
if (callMetadata.log.length) if (callMetadata.log.length)
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote); rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
this.onmessage({ id, error: serializeError(e) }); this.onmessage({ id, error: serializeError(e) });

View File

@ -345,8 +345,8 @@ export abstract class BrowserContext extends SdkObject {
} }
} }
async extendInjectedScript(source: string) { async extendInjectedScript(source: string, arg?: any) {
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source).catch(e => {}); const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {});
const installInPage = (page: Page) => { const installInPage = (page: Page) => {
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame); page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
return Promise.all(page.frames().map(installInFrame)); return Promise.all(page.frames().map(installInFrame));

View File

@ -39,7 +39,7 @@ export type CallMetadata = {
params: any; params: any;
stack?: StackFrame[]; stack?: StackFrame[];
log: string[]; log: string[];
error?: Error; error?: string;
point?: Point; point?: Point;
}; };

View File

@ -51,8 +51,10 @@ export class Recorder {
private _actionPointElement: HTMLElement; private _actionPointElement: HTMLElement;
private _actionPoint: Point | undefined; private _actionPoint: Point | undefined;
private _actionSelector: string | undefined; private _actionSelector: string | undefined;
private _params: { isUnderTest: boolean; };
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
this._params = params;
this._injectedScript = injectedScript; this._injectedScript = injectedScript;
this._outerGlassPaneElement = html` this._outerGlassPaneElement = html`
<x-pw-glass style=" <x-pw-glass style="
@ -76,7 +78,7 @@ export class Recorder {
</x-pw-glass-inner>`; </x-pw-glass-inner>`;
// Use a closed shadow root to prevent selectors matching our internal previews. // Use a closed shadow root to prevent selectors matching our internal previews.
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' }); this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: this._params.isUnderTest ? 'open' : 'closed' });
this._glassPaneShadow.appendChild(this._innerGlassPaneElement); this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
this._glassPaneShadow.appendChild(this._actionPointElement); this._glassPaneShadow.appendChild(this._actionPointElement);
this._glassPaneShadow.appendChild(html` this._glassPaneShadow.appendChild(html`

View File

@ -26,6 +26,7 @@ import { internalCallMetadata } from '../../instrumentation';
import type { CallLog, EventData, Mode, Source } from './recorderTypes'; import type { CallLog, EventData, Mode, Source } from './recorderTypes';
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { isUnderTest } from '../../../utils/utils'; import { isUnderTest } from '../../../utils/utils';
import { RecentLogsCollector } from '../../../utils/debugLogger';
const readFileAsync = util.promisify(fs.readFile); const readFileAsync = util.promisify(fs.readFile);
@ -41,11 +42,13 @@ declare global {
export class RecorderApp extends EventEmitter { export class RecorderApp extends EventEmitter {
private _page: Page; private _page: Page;
readonly wsEndpoint: string | undefined;
constructor(page: Page) { constructor(page: Page, wsEndpoint: string | undefined) {
super(); super();
this.setMaxListeners(0); this.setMaxListeners(0);
this._page = page; this._page = page;
this.wsEndpoint = wsEndpoint;
} }
async close() { async close() {
@ -90,28 +93,45 @@ export class RecorderApp extends EventEmitter {
static async open(inspectedContext: BrowserContext): Promise<RecorderApp> { static async open(inspectedContext: BrowserContext): Promise<RecorderApp> {
const recorderPlaywright = createPlaywright(true); const recorderPlaywright = createPlaywright(true);
const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', { const args = [
sdkLanguage: inspectedContext._options.sdkLanguage,
args: [
'--app=data:text/html,', '--app=data:text/html,',
'--window-size=600,600', '--window-size=600,600',
'--window-position=1280,10', '--window-position=1280,10',
], ];
if (isUnderTest())
args.push(`--remote-debugging-port=0`);
const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', {
sdkLanguage: inspectedContext._options.sdkLanguage,
args,
noDefaultViewport: true, noDefaultViewport: true,
headless: isUnderTest() && !inspectedContext._browser.options.headful headless: isUnderTest() && !inspectedContext._browser.options.headful
}); });
const wsEndpoint = isUnderTest() ? await this._parseWsEndpoint(context._browser.options.browserLogsCollector) : undefined;
const controller = new ProgressController(internalCallMetadata(), context._browser); const controller = new ProgressController(internalCallMetadata(), context._browser);
await controller.run(async progress => { await controller.run(async progress => {
await context._browser._defaultContext!._loadDefaultContextAsIs(progress); await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
}); });
const [page] = context.pages(); const [page] = context.pages();
const result = new RecorderApp(page); const result = new RecorderApp(page, wsEndpoint);
await result._init(); await result._init();
return result; return result;
} }
private static async _parseWsEndpoint(recentLogs: RecentLogsCollector): Promise<string> {
let callback: ((log: string) => void) | undefined;
const result = new Promise<string>(f => callback = f);
const check = (log: string) => {
const match = log.match(/DevTools listening on (.*)/);
if (match)
callback!(match[1]);
};
for (const log of recentLogs.recentLogs())
check(log);
recentLogs.on('log', check);
return result;
}
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> { async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((mode: Mode) => { await this._page.mainFrame()._evaluateExpression(((mode: Mode) => {
window.playwrightSetMode(mode); window.playwrightSetMode(mode);

View File

@ -34,11 +34,12 @@ export type CallLog = {
title: string; title: string;
messages: string[]; messages: string[];
status: 'in-progress' | 'done' | 'error' | 'paused'; status: 'in-progress' | 'done' | 'error' | 'paused';
error?: string;
}; };
export type SourceHighlight = { export type SourceHighlight = {
line: number; line: number;
type: 'running' | 'paused'; type: 'running' | 'paused' | 'error';
}; };
export type Source = { export type Source = {

View File

@ -33,6 +33,7 @@ import { RecorderApp } from './recorder/recorderApp';
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation'; import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
import { Point } from '../../common/types'; import { Point } from '../../common/types';
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
import { isUnderTest } from '../../utils/utils';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -179,7 +180,7 @@ export class RecorderSupplement {
this._resume(false).catch(() => {}); this._resume(false).catch(() => {});
}); });
await this._context.extendInjectedScript(recorderSource.source); await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest() });
await this._context.extendInjectedScript(consoleApiSource.source); await this._context.extendInjectedScript(consoleApiSource.source);
(this._context as any).recorderAppForTest = recorderApp; (this._context as any).recorderAppForTest = recorderApp;
@ -332,6 +333,7 @@ export class RecorderSupplement {
} }
async onAfterCall(metadata: CallMetadata): Promise<void> { async onAfterCall(metadata: CallMetadata): Promise<void> {
if (!metadata.error)
this._currentCallsMetadata.delete(metadata); this._currentCallsMetadata.delete(metadata);
this._pausedCallsMetadata.delete(metadata); this._pausedCallsMetadata.delete(metadata);
this._updateUserSources(); this._updateUserSources();
@ -357,7 +359,7 @@ export class RecorderSupplement {
} }
if (line) { if (line) {
const paused = this._pausedCallsMetadata.has(metadata); const paused = this._pausedCallsMetadata.has(metadata);
source.highlight.push({ line, type: paused ? 'paused' : 'running' }); source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
if (paused) if (paused)
source.revealLine = line; source.revealLine = line;
} }
@ -387,7 +389,7 @@ export class RecorderSupplement {
status = 'paused'; status = 'paused';
if (metadata.error) if (metadata.error)
status = 'error'; status = 'error';
logs.push({ id: metadata.id, messages: metadata.log, title, status }); logs.push({ id: metadata.id, messages: metadata.log, title, status, error: metadata.error });
} }
this._recorderApp?.updateCallLogs(logs); this._recorderApp?.updateCallLogs(logs);
} }

View File

@ -180,7 +180,7 @@ class ContextTracer implements SnapshotterDelegate {
startTime: metadata.startTime, startTime: metadata.startTime,
endTime: metadata.endTime, endTime: metadata.endTime,
logs: metadata.log.slice(), logs: metadata.log.slice(),
error: metadata.error ? metadata.error.stack : undefined, error: metadata.error,
snapshots: snapshotsForMetadata(metadata), snapshots: snapshotsForMetadata(metadata),
}; };
this._appendTraceEvent(event); this._appendTraceEvent(event);

View File

@ -16,6 +16,7 @@
import debug from 'debug'; import debug from 'debug';
import fs from 'fs'; import fs from 'fs';
import { EventEmitter } from 'events';
const debugLoggerColorMap = { const debugLoggerColorMap = {
'api': 45, // cyan 'api': 45, // cyan
@ -63,10 +64,11 @@ class DebugLogger {
export const debugLogger = new DebugLogger(); export const debugLogger = new DebugLogger();
const kLogCount = 50; const kLogCount = 50;
export class RecentLogsCollector { export class RecentLogsCollector extends EventEmitter {
private _logs: string[] = []; private _logs: string[] = [];
log(message: string) { log(message: string) {
this.emit('log', message);
this._logs.push(message); this._logs.push(message);
if (this._logs.length === kLogCount * 2) if (this._logs.length === kLogCount * 2)
this._logs.splice(0, kLogCount); this._logs.splice(0, kLogCount);

View File

@ -45,12 +45,18 @@
} }
.source-line-running { .source-line-running {
background-color: #6fa8dc7f; background-color: #b3dbff7f;
z-index: 2; z-index: 2;
} }
.source-line-paused { .source-line-paused {
background-color: #ffc0cb7f; background-color: #b3dbff7f;
outline: 1px solid red; outline: 1px solid #009aff;
z-index: 2;
}
.source-line-error {
background-color: #fff0f0;
outline: 1px solid #ffd6d6;
z-index: 2; z-index: 2;
} }

View File

@ -21,7 +21,7 @@ import '../../third_party/highlightjs/highlightjs/tomorrow.css';
export type SourceHighlight = { export type SourceHighlight = {
line: number; line: number;
type: 'running' | 'paused'; type: 'running' | 'paused' | 'error';
}; };
export interface SourceProps { export interface SourceProps {

View File

@ -42,15 +42,11 @@
.recorder-log-message { .recorder-log-message {
flex: none; flex: none;
padding: 3px 12px; padding: 3px 0 3px 36px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.recorder-log-message-sub-level {
padding-left: 40px;
}
.recorder-log-header { .recorder-log-header {
color: var(--toolbar-color); color: var(--toolbar-color);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
@ -63,18 +59,36 @@
} }
.recorder-log-call { .recorder-log-call {
color: var(--toolbar-color); display: flex;
background-color: var(--toolbar-bg-color); flex: none;
border-top: 1px solid #ddd; flex-direction: column;
border-bottom: 1px solid #eee; border-top: 1px solid #eee;
}
.recorder-log-call-header {
height: 24px; height: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 9px; padding: 0 2px;
margin-bottom: 3px;
z-index: 2; z-index: 2;
} }
.recorder-log-call .codicon { .recorder-log-call .codicon {
padding: 0 4px; padding: 0 4px;
} }
.recorder-log .codicon-check {
color: #21a945;
font-weight: bold;
}
.recorder-log-call.error {
background-color: #fff0f0;
border-top: 1px solid #ffd6d6;
}
.recorder-log-call.error .recorder-log-call-header,
.recorder-log-message.error,
.recorder-log .codicon-error {
color: red;
}

View File

@ -79,12 +79,14 @@ export const Recorder: React.FC<RecorderProps> = ({
copy(source.text); copy(source.text);
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
setPaused(false);
window.dispatch({ event: 'resume' }).catch(() => {}); window.dispatch({ event: 'resume' }).catch(() => {});
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => { <ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => {
window.dispatch({ event: 'pause' }).catch(() => {}); window.dispatch({ event: 'pause' }).catch(() => {});
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => {
setPaused(false);
window.dispatch({ event: 'step' }).catch(() => {}); window.dispatch({ event: 'step' }).catch(() => {});
}}></ToolbarButton> }}></ToolbarButton>
<div style={{flex: 'auto'}}></div> <div style={{flex: 'auto'}}></div>
@ -98,15 +100,18 @@ export const Recorder: React.FC<RecorderProps> = ({
<div className='recorder-log-header' style={{flex: 'none'}}>Log</div> <div className='recorder-log-header' style={{flex: 'none'}}>Log</div>
<div className='recorder-log' style={{flex: 'auto'}}> <div className='recorder-log' style={{flex: 'auto'}}>
{[...log.values()].map(callLog => { {[...log.values()].map(callLog => {
return <div className='vbox' style={{flex: 'none'}} key={callLog.id}> return <div className={`recorder-log-call ${callLog.status}`} key={callLog.id}>
<div className='recorder-log-call'> <div className='recorder-log-call-header'>
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title } <span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
</div> </div>
{ callLog.messages.map((message, i) => { { callLog.messages.map((message, i) => {
return <div className='recorder-log-message' key={i}> return <div className='recorder-log-message' key={i}>
{ message } { message.trim() }
</div>; </div>;
})} })}
{ callLog.error ? <div className='recorder-log-message error'>
{ callLog.error }
</div> : undefined }
</div> </div>
})} })}
<div ref={messagesEndRef}></div> <div ref={messagesEndRef}></div>

View File

@ -34,10 +34,10 @@ type TestFixtures = {
export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>(); export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
fixtures.recorder.init(async ({ page, recorderFrame }, runTest) => { fixtures.recorder.init(async ({ page, recorderPageGetter }, runTest) => {
await (page.context() as any)._enableRecorder({ language: 'javascript', startRecording: true }); await (page.context() as any)._enableRecorder({ language: 'javascript', startRecording: true });
const recorderFrameInstance = await recorderFrame(); const recorderPage = await recorderPageGetter();
await runTest(new Recorder(page, recorderFrameInstance)); await runTest(new Recorder(page, recorderPage));
}); });
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => { fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
@ -65,12 +65,12 @@ class Recorder {
_highlightInstalled: boolean _highlightInstalled: boolean
_actionReporterInstalled: boolean _actionReporterInstalled: boolean
_actionPerformedCallback: Function _actionPerformedCallback: Function
recorderFrame: any; recorderPage: Page;
private _text: string = ''; private _text: string = '';
constructor(page: Page, recorderFrame: any) { constructor(page: Page, recorderPage: Page) {
this.page = page; this.page = page;
this.recorderFrame = recorderFrame; this.recorderPage = recorderPage;
this._highlightCallback = () => { }; this._highlightCallback = () => { };
this._highlightInstalled = false; this._highlightInstalled = false;
this._actionReporterInstalled = false; this._actionReporterInstalled = false;
@ -98,7 +98,7 @@ class Recorder {
} }
async waitForOutput(text: string): Promise<void> { async waitForOutput(text: string): Promise<void> {
this._text = await this.recorderFrame._evaluateExpression(((text: string) => { this._text = await this.recorderPage.evaluate((text: string) => {
const w = window as any; const w = window as any;
return new Promise(f => { return new Promise(f => {
const poll = () => { const poll = () => {
@ -110,7 +110,7 @@ class Recorder {
}; };
setTimeout(poll); setTimeout(poll);
}); });
}).toString(), true, text, 'main'); }, text);
} }
output(): string { output(): string {

View File

@ -21,39 +21,102 @@ const { it, describe} = folio;
describe('pause', (suite, { mode }) => { describe('pause', (suite, { mode }) => {
suite.skip(mode !== 'default'); suite.skip(mode !== 'default');
}, () => { }, () => {
it('should pause and resume the script', async ({ page, recorderClick }) => { it('should pause and resume the script', async ({ page, recorderPageGetter }) => {
await Promise.all([ const scriptPromise = (async () => {
page.pause(), await page.pause();
recorderClick('[title=Resume]') })();
]); const recorderPage = await recorderPageGetter();
await recorderPage.click('[title=Resume]');
await scriptPromise;
}); });
it('should resume from console', async ({page}) => { it('should resume from console', async ({page}) => {
const scriptPromise = (async () => {
await page.pause();
})();
await Promise.all([ await Promise.all([
page.pause(),
page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => { page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => {
return page.evaluate('window.playwright.resume()'); return page.evaluate('window.playwright.resume()');
}) })
]); ]);
await scriptPromise;
}); });
it('should pause after a navigation', async ({page, server, recorderClick}) => { it('should pause after a navigation', async ({page, server, recorderPageGetter}) => {
const scriptPromise = (async () => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await Promise.all([ await page.pause();
page.pause(), })();
recorderClick('[title=Resume]') const recorderPage = await recorderPageGetter();
]); await recorderPage.click('[title=Resume]');
await scriptPromise;
}); });
it('should show source', async ({page, server, recorderClick, recorderFrame}) => { it('should show source', async ({page, recorderPageGetter}) => {
await page.goto(server.EMPTY_PAGE); const scriptPromise = (async () => {
const pausePromise = page.pause(); await page.pause();
const frame = await recorderFrame(); })();
const source = await frame._evaluateExpression((() => { const recorderPage = await recorderPageGetter();
return document.querySelector('.source-line-paused .source-code').textContent; const source = await recorderPage.textContent('.source-line-paused .source-code');
}).toString(), true, undefined, 'main');
expect(source).toContain('page.pause()'); expect(source).toContain('page.pause()');
await recorderClick('[title=Resume]'); await recorderPage.click('[title=Resume]');
await pausePromise; await scriptPromise;
});
it('should pause on next pause', async ({page, recorderPageGetter}) => {
const scriptPromise = (async () => {
await page.pause(); // 1
await page.pause(); // 2
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause(); // 1');
await recorderPage.click('[title=Resume]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});
it('should step', async ({page, recorderPageGetter}) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await page.click('button');
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause();');
await recorderPage.click('[title="Step over"]');
await recorderPage.waitForSelector('.source-line-paused :has-text("page.click")');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});
it('should highlight pointer', async ({page, recorderPageGetter}) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await page.click('button');
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over"]');
const point = await page.waitForSelector('x-pw-action-point');
const button = await page.waitForSelector('button');
const box1 = await button.boundingBox();
const box2 = await point.boundingBox();
const x1 = box1.x + box1.width / 2;
const y1 = box1.y + box1.height / 2;
const x2 = box2.x + box2.width / 2;
const y2 = box2.y + box2.height / 2;
expect(Math.abs(x1 - x2) < 2).toBeTruthy();
expect(Math.abs(y1 - y2) < 2).toBeTruthy();
await recorderPage.click('[title="Step over"]');
await scriptPromise;
}); });
}); });

View File

@ -15,25 +15,21 @@
*/ */
import { folio as baseFolio } from './fixtures'; import { folio as baseFolio } from './fixtures';
import { internalCallMetadata } from '../lib/server/instrumentation'; import { Page } from '..';
import { chromium } from '../index';
const fixtures = baseFolio.extend<{ const fixtures = baseFolio.extend<{
recorderFrame: () => Promise<any>, recorderPageGetter: () => Promise<Page>,
recorderClick: (selector: string) => Promise<void>
}>(); }>();
fixtures.recorderFrame.init(async ({context, toImpl}, runTest) => { fixtures.recorderPageGetter.init(async ({context, toImpl}, runTest) => {
await runTest(async () => { await runTest(async () => {
while (!toImpl(context).recorderAppForTest) while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100)); await new Promise(f => setTimeout(f, 100));
return toImpl(context).recorderAppForTest._page.mainFrame(); const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint;
}); const browser = await chromium.connectOverCDP({ wsEndpoint });
}); const c = browser.contexts()[0];
return c.pages()[0] || await c.waitForEvent('page');
fixtures.recorderClick.init(async ({ recorderFrame }, runTest) => {
await runTest(async (selector: string) => {
const frame = await recorderFrame();
await frame.click(internalCallMetadata(), selector, {});
}); });
}); });