/** * @license * Copyright 2023 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import expect from 'expect'; import {TimeoutError} from 'puppeteer-core'; import { Locator, LocatorEvent, } from 'puppeteer-core/internal/api/locators/locators.js'; import sinon from 'sinon'; import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; describe('Locator', function () { setupTestBrowserHooks(); it('should work with a frame', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); let willClick = false; await page .mainFrame() .locator('button') .on(LocatorEvent.Action, () => { willClick = true; }) .click(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); expect(willClick).toBe(true); }); it('should work without preconditions', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); let willClick = false; await page .locator('button') .setEnsureElementIsInTheViewport(false) .setTimeout(0) .setVisibility(null) .setWaitForEnabled(false) .setWaitForStableBoundingBox(false) .on(LocatorEvent.Action, () => { willClick = true; }) .click(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); expect(willClick).toBe(true); }); describe('Locator.click', function () { it('should work', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); let willClick = false; await page .locator('button') .on(LocatorEvent.Action, () => { willClick = true; }) .click(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); expect(willClick).toBe(true); }); it('should work for multiple selectors', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); let clicked = false; await page .locator('::-p-text(test), ::-p-xpath(/button)') .on(LocatorEvent.Action, () => { clicked = true; }) .click(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); expect(clicked).toBe(true); }); it('should work if the element is out of viewport', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); await page.locator('button').click(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); }); it('should work if the element becomes visible later', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); using button = await page.$('button'); const result = page .locator('button') .click() .catch(err => { return err; }); expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('test'); await button?.evaluate(el => { el.style.display = 'block'; }); const maybeError = await result; if (maybeError instanceof Error) { throw maybeError; } expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('clicked'); }); it('should work if the element becomes enabled later', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); using button = await page.$('button'); const result = page.locator('button').click(); expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('test'); await button?.evaluate(el => { el.disabled = false; }); await result; expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('clicked'); }); it('should work if multiple conditions are satisfied later', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); using button = await page.$('button'); const result = page.locator('button').click(); expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('test'); await button?.evaluate(el => { el.disabled = false; el.style.display = 'block'; }); await result; expect( await button?.evaluate(el => { return el.innerText; }) ).toBe('clicked'); }); it('should time out', async () => { const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { const {page} = await getTestState(); page.setDefaultTimeout(5000); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); const result = page.locator('button').click(); clock.tick(5100); await expect(result).rejects.toEqual( new TimeoutError('Timed out after waiting 5000ms') ); } finally { clock.restore(); } }); it('should retry clicks on errors', async () => { const {page} = await getTestState(); const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { page.setDefaultTimeout(5000); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); const result = page.locator('button').click(); clock.tick(5100); await expect(result).rejects.toEqual( new TimeoutError('Timed out after waiting 5000ms') ); } finally { clock.restore(); } }); it('can be aborted', async () => { const {page} = await getTestState(); const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { page.setDefaultTimeout(5000); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); const abortController = new AbortController(); const result = page.locator('button').click({ signal: abortController.signal, }); clock.tick(2000); abortController.abort(); await expect(result).rejects.toThrow(/aborted/); } finally { clock.restore(); } }); it('should work with a OOPIF', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); const frame = await page.waitForFrame(frame => { return frame.url().startsWith('data'); }); let willClick = false; await frame .locator('button') .on(LocatorEvent.Action, () => { willClick = true; }) .click(); using button = await frame.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('clicked'); expect(willClick).toBe(true); }); }); describe('Locator.hover', function () { it('should work', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); let hovered = false; await page .locator('button') .on(LocatorEvent.Action, () => { hovered = true; }) .hover(); using button = await page.$('button'); const text = await button?.evaluate(el => { return el.innerText; }); expect(text).toBe('hovered'); expect(hovered).toBe(true); }); }); describe('Locator.scroll', function () { it('should work', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(`
test
`); let scrolled = false; await page .locator('div') .on(LocatorEvent.Action, () => { scrolled = true; }) .scroll({ scrollTop: 500, scrollLeft: 500, }); using scrollable = await page.$('div'); const scroll = await scrollable?.evaluate(el => { return el.scrollTop + ' ' + el.scrollLeft; }); expect(scroll).toBe('500 500'); expect(scrolled).toBe(true); }); }); describe('Locator.fill', function () { it('should work for textarea', async () => { const {page} = await getTestState(); await page.setContent(` `); let filled = false; await page .locator('textarea') .on(LocatorEvent.Action, () => { filled = true; }) .fill('test'); expect( await page.evaluate(() => { return document.querySelector('textarea')?.value === 'test'; }) ).toBe(true); expect(filled).toBe(true); }); it('should work for selects', async () => { const {page} = await getTestState(); await page.setContent(` `); let filled = false; await page .locator('select') .on(LocatorEvent.Action, () => { filled = true; }) .fill('value2'); expect( await page.evaluate(() => { return document.querySelector('select')?.value === 'value2'; }) ).toBe(true); expect(filled).toBe(true); }); it('should work for inputs', async () => { const {page} = await getTestState(); await page.setContent(` `); await page.locator('input').fill('test'); expect( await page.evaluate(() => { return document.querySelector('input')?.value === 'test'; }) ).toBe(true); }); it('should work if the input becomes enabled later', async () => { const {page} = await getTestState(); await page.setContent(` `); using input = await page.$('input'); const result = page.locator('input').fill('test'); expect( await input?.evaluate(el => { return el.value; }) ).toBe(''); await input?.evaluate(el => { el.disabled = false; }); await result; expect( await input?.evaluate(el => { return el.value; }) ).toBe('test'); }); it('should work for contenteditable', async () => { const {page} = await getTestState(); await page.setContent(`
`); await page.locator('div').fill('test'); expect( await page.evaluate(() => { return document.querySelector('div')?.innerText === 'test'; }) ).toBe(true); }); it('should work for pre-filled inputs', async () => { const {page} = await getTestState(); await page.setContent(` `); await page.locator('input').fill('test'); expect( await page.evaluate(() => { return document.querySelector('input')?.value === 'test'; }) ).toBe(true); }); it('should override pre-filled inputs', async () => { const {page} = await getTestState(); await page.setContent(` `); await page.locator('input').fill('test'); expect( await page.evaluate(() => { return document.querySelector('input')?.value === 'test'; }) ).toBe(true); }); it('should work for non-text inputs', async () => { const {page} = await getTestState(); await page.setContent(` `); await page.locator('input').fill('#333333'); expect( await page.evaluate(() => { return document.querySelector('input')?.value === '#333333'; }) ).toBe(true); }); }); describe('Locator.race', () => { it('races multiple locators', async () => { const {page} = await getTestState(); await page.setViewport({width: 500, height: 500}); await page.setContent(` `); await page.evaluate(() => { // @ts-expect-error different context. window.count = 0; }); await Locator.race([ page.locator('button'), page.locator('button'), ]).click(); const count = await page.evaluate(() => { // @ts-expect-error different context. return globalThis.count; }); expect(count).toBe(1); }); it('can be aborted', async () => { const {page} = await getTestState(); const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { await page.setViewport({width: 500, height: 500}); await page.setContent(` `); const abortController = new AbortController(); const result = Locator.race([ page.locator('button'), page.locator('button'), ]) .setTimeout(5000) .click({ signal: abortController.signal, }); clock.tick(2000); abortController.abort(); await expect(result).rejects.toThrow(/aborted/); } finally { clock.restore(); } }); it('should time out when all locators do not match', async () => { const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { const {page} = await getTestState(); await page.setContent(``); const result = Locator.race([ page.locator('not-found'), page.locator('not-found'), ]) .setTimeout(5000) .click(); clock.tick(5100); await expect(result).rejects.toEqual( new TimeoutError('Timed out after waiting 5000ms') ); } finally { clock.restore(); } }); it('should not time out when one of the locators matches', async () => { const {page} = await getTestState(); await page.setContent(``); const result = Locator.race([ page.locator('not-found'), page.locator('button'), ]).click(); await expect(result).resolves.toEqual(undefined); }); }); describe('Locator.prototype.map', () => { it('should work', async () => { const {page} = await getTestState(); await page.setContent(`
test
`); await expect( page .locator('::-p-text(test)') .map(element => { return element.getAttribute('clickable'); }) .wait() ).resolves.toEqual(null); await page.evaluate(() => { document.querySelector('div')?.setAttribute('clickable', 'true'); }); await expect( page .locator('::-p-text(test)') .map(element => { return element.getAttribute('clickable'); }) .wait() ).resolves.toEqual('true'); }); it('should work with throws', async () => { const {page} = await getTestState(); await page.setContent(`
test
`); const result = page .locator('::-p-text(test)') .map(element => { const clickable = element.getAttribute('clickable'); if (!clickable) { throw new Error('Missing `clickable` as an attribute'); } return clickable; }) .wait(); await page.evaluate(() => { document.querySelector('div')?.setAttribute('clickable', 'true'); }); await expect(result).resolves.toEqual('true'); }); it('should work with expect', async () => { const {page} = await getTestState(); await page.setContent(`
test
`); const result = page .locator('::-p-text(test)') .filter(element => { return element.getAttribute('clickable') !== null; }) .map(element => { return element.getAttribute('clickable'); }) .wait(); await page.evaluate(() => { document.querySelector('div')?.setAttribute('clickable', 'true'); }); await expect(result).resolves.toEqual('true'); }); }); describe('Locator.prototype.filter', () => { it('should resolve as soon as the predicate matches', async () => { const clock = sinon.useFakeTimers({ shouldClearNativeTimers: true, shouldAdvanceTime: true, }); try { const {page} = await getTestState(); await page.setContent(`
test
`); const result = page .locator('::-p-text(test)') .setTimeout(5000) .filter(async element => { return element.getAttribute('clickable') === 'true'; }) .filter(element => { return element.getAttribute('clickable') === 'true'; }) .hover(); clock.tick(2000); await page.evaluate(() => { document.querySelector('div')?.setAttribute('clickable', 'true'); }); clock.restore(); await expect(result).resolves.toEqual(undefined); } finally { clock.restore(); } }); }); describe('Locator.prototype.wait', () => { it('should work', async () => { const {page} = await getTestState(); void page.setContent(` `); // This shouldn't throw. await page.locator('div').wait(); }); }); describe('Locator.prototype.waitHandle', () => { it('should work', async () => { const {page} = await getTestState(); void page.setContent(` `); await expect(page.locator('div').waitHandle()).resolves.toBeDefined(); }); }); describe('Locator.prototype.clone', () => { it('should work', async () => { const {page} = await getTestState(); const locator = page.locator('div'); const clone = locator.clone(); expect(locator).not.toStrictEqual(clone); }); it('should work internally with delegated locators', async () => { const {page} = await getTestState(); const locator = page.locator('div'); const delegatedLocators = [ locator.map(div => { return div.textContent; }), locator.filter(div => { return div.textContent?.length === 0; }), ]; for (let delegatedLocator of delegatedLocators) { delegatedLocator = delegatedLocator.setTimeout(500); expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout); } }); }); describe('FunctionLocator', () => { it('should work', async () => { const {page} = await getTestState(); const result = page .locator(() => { return new Promise(resolve => { return setTimeout(() => { return resolve(true); }, 100); }); }) .wait(); await expect(result).resolves.toEqual(true); }); it('should work with actions', async () => { const {page} = await getTestState(); await page.setContent(`
test
`); await page .locator(() => { return document.getElementsByTagName('div')[0] as HTMLDivElement; }) .click(); await expect( page.evaluate(() => { return (window as unknown as {clicked: boolean}).clicked; }) ).resolves.toEqual(true); }); }); });