Skip to content

Commit e8e45e8

Browse files
authored
feat(dom): migrate innerText, innerHTML and getAttribute to tasks (#2782)
This ensures synchronous access to avoid element recycling.
1 parent ff1fe3a commit e8e45e8

File tree

3 files changed

+141
-9
lines changed

3 files changed

+141
-9
lines changed

src/dom.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ export function toFileTransferPayload(files: types.FilePayload[]): types.FileTra
734734
}));
735735
}
736736

737-
function throwFatalDOMError<T>(result: T | FatalDOMError): T {
737+
export function throwFatalDOMError<T>(result: T | FatalDOMError): T {
738738
if (result === 'error:notelement')
739739
throw new Error('Node is not an element');
740740
if (result === 'error:nothtmlelement')
@@ -807,9 +807,10 @@ export function dispatchEventTask(selector: SelectorInfo, type: string, eventIni
807807
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, type, eventInit }) => {
808808
return injected.pollRaf((progress, continuePolling) => {
809809
const element = injected.querySelector(parsed, document);
810-
if (element)
811-
injected.dispatchEvent(element, type, eventInit);
812-
return element ? undefined : continuePolling;
810+
if (!element)
811+
return continuePolling;
812+
progress.log(` selector resolved to ${injected.previewNode(element)}`);
813+
injected.dispatchEvent(element, type, eventInit);
813814
});
814815
}, { parsed: selector.parsed, type, eventInit });
815816
}
@@ -818,7 +819,48 @@ export function textContentTask(selector: SelectorInfo): SchedulableTask<string
818819
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
819820
return injected.pollRaf((progress, continuePolling) => {
820821
const element = injected.querySelector(parsed, document);
821-
return element ? element.textContent : continuePolling;
822+
if (!element)
823+
return continuePolling;
824+
progress.log(` selector resolved to ${injected.previewNode(element)}`);
825+
return element.textContent;
822826
});
823827
}, selector.parsed);
824828
}
829+
830+
export function innerTextTask(selector: SelectorInfo): SchedulableTask<'error:nothtmlelement' | { innerText: string }> {
831+
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
832+
return injected.pollRaf((progress, continuePolling) => {
833+
const element = injected.querySelector(parsed, document);
834+
if (!element)
835+
return continuePolling;
836+
progress.log(` selector resolved to ${injected.previewNode(element)}`);
837+
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml')
838+
return 'error:nothtmlelement';
839+
return { innerText: (element as HTMLElement).innerText };
840+
});
841+
}, selector.parsed);
842+
}
843+
844+
export function innerHTMLTask(selector: SelectorInfo): SchedulableTask<string> {
845+
return injectedScript => injectedScript.evaluateHandle((injected, parsed) => {
846+
return injected.pollRaf((progress, continuePolling) => {
847+
const element = injected.querySelector(parsed, document);
848+
if (!element)
849+
return continuePolling;
850+
progress.log(` selector resolved to ${injected.previewNode(element)}`);
851+
return element.innerHTML;
852+
});
853+
}, selector.parsed);
854+
}
855+
856+
export function getAttributeTask(selector: SelectorInfo, name: string): SchedulableTask<string | null> {
857+
return injectedScript => injectedScript.evaluateHandle((injected, { parsed, name }) => {
858+
return injected.pollRaf((progress, continuePolling) => {
859+
const element = injected.querySelector(parsed, document);
860+
if (!element)
861+
return continuePolling;
862+
progress.log(` selector resolved to ${injected.previewNode(element)}`);
863+
return element.getAttribute(name);
864+
});
865+
}, { parsed: selector.parsed, name });
866+
}

src/frames.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,21 +759,37 @@ export class Frame {
759759
const info = selectors._parseSelector(selector);
760760
const task = dom.textContentTask(info);
761761
return this._page._runAbortableTask(async progress => {
762-
progress.logger.info(`Retrieving text context from "${selector}"...`);
762+
progress.logger.info(` retrieving textContent from "${selector}"`);
763763
return this._scheduleRerunnableTask(progress, info.world, task);
764764
}, this._page._timeoutSettings.timeout(options), this._apiName('textContent'));
765765
}
766766

767767
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
768-
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText(), this._apiName('innerText'));
768+
const info = selectors._parseSelector(selector);
769+
const task = dom.innerTextTask(info);
770+
return this._page._runAbortableTask(async progress => {
771+
progress.logger.info(` retrieving innerText from "${selector}"`);
772+
const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
773+
return result.innerText;
774+
}, this._page._timeoutSettings.timeout(options), this._apiName('innerText'));
769775
}
770776

771777
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
772-
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML(), this._apiName('innerHTML'));
778+
const info = selectors._parseSelector(selector);
779+
const task = dom.innerHTMLTask(info);
780+
return this._page._runAbortableTask(async progress => {
781+
progress.logger.info(` retrieving innerHTML from "${selector}"`);
782+
return this._scheduleRerunnableTask(progress, info.world, task);
783+
}, this._page._timeoutSettings.timeout(options), this._apiName('innerHTML'));
773784
}
774785

775786
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
776-
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name), this._apiName('getAttribute'));
787+
const info = selectors._parseSelector(selector);
788+
const task = dom.getAttributeTask(info, name);
789+
return this._page._runAbortableTask(async progress => {
790+
progress.logger.info(` retrieving attribute "${name}" from "${selector}"`);
791+
return this._scheduleRerunnableTask(progress, info.world, task);
792+
}, this._page._timeoutSettings.timeout(options), this._apiName('getAttribute'));
777793
}
778794

779795
async hover(selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {

test/elementhandle.spec.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,14 @@ describe('ElementHandle convenience API', function() {
470470
expect(await handle.innerText()).toBe('Text, more text');
471471
expect(await page.innerText('#inner')).toBe('Text, more text');
472472
});
473+
it('innerText should throw', async({page, server}) => {
474+
await page.setContent(`<svg>text</svg>`);
475+
const error1 = await page.innerText('svg').catch(e => e);
476+
expect(error1.message).toContain('Not an HTMLElement');
477+
const handle = await page.$('svg');
478+
const error2 = await handle.innerText().catch(e => e);
479+
expect(error2.message).toContain('Not an HTMLElement');
480+
});
473481
it('textContent should work', async({page, server}) => {
474482
await page.goto(`${server.PREFIX}/dom.html`);
475483
const handle = await page.$('#inner');
@@ -498,6 +506,72 @@ describe('ElementHandle convenience API', function() {
498506
expect(tc).toBe('Hello');
499507
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
500508
});
509+
it('innerText should be atomic', async({page}) => {
510+
const createDummySelector = () => ({
511+
create(root, target) {},
512+
query(root, selector) {
513+
const result = root.querySelector(selector);
514+
if (result)
515+
Promise.resolve().then(() => result.textContent = 'modified');
516+
return result;
517+
},
518+
queryAll(root, selector) {
519+
const result = Array.from(root.querySelectorAll(selector));
520+
for (const e of result)
521+
Promise.resolve().then(() => result.textContent = 'modified');
522+
return result;
523+
}
524+
});
525+
await utils.registerEngine('innerText', createDummySelector);
526+
await page.setContent(`<div>Hello</div>`);
527+
const tc = await page.innerText('innerText=div');
528+
expect(tc).toBe('Hello');
529+
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
530+
});
531+
it('innerHTML should be atomic', async({page}) => {
532+
const createDummySelector = () => ({
533+
create(root, target) {},
534+
query(root, selector) {
535+
const result = root.querySelector(selector);
536+
if (result)
537+
Promise.resolve().then(() => result.textContent = 'modified');
538+
return result;
539+
},
540+
queryAll(root, selector) {
541+
const result = Array.from(root.querySelectorAll(selector));
542+
for (const e of result)
543+
Promise.resolve().then(() => result.textContent = 'modified');
544+
return result;
545+
}
546+
});
547+
await utils.registerEngine('innerHTML', createDummySelector);
548+
await page.setContent(`<div>Hello<span>world</span></div>`);
549+
const tc = await page.innerHTML('innerHTML=div');
550+
expect(tc).toBe('Hello<span>world</span>');
551+
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
552+
});
553+
it('getAttribute should be atomic', async({page}) => {
554+
const createDummySelector = () => ({
555+
create(root, target) {},
556+
query(root, selector) {
557+
const result = root.querySelector(selector);
558+
if (result)
559+
Promise.resolve().then(() => result.setAttribute('foo', 'modified'));
560+
return result;
561+
},
562+
queryAll(root, selector) {
563+
const result = Array.from(root.querySelectorAll(selector));
564+
for (const e of result)
565+
Promise.resolve().then(() => result.setAttribute('foo', 'modified'));
566+
return result;
567+
}
568+
});
569+
await utils.registerEngine('getAttribute', createDummySelector);
570+
await page.setContent(`<div foo=hello></div>`);
571+
const tc = await page.getAttribute('getAttribute=div', 'foo');
572+
expect(tc).toBe('hello');
573+
expect(await page.evaluate(() => document.querySelector('div').getAttribute('foo'))).toBe('modified');
574+
});
501575
});
502576

503577
describe('ElementHandle.check', () => {

0 commit comments

Comments
 (0)