From 993f0195499f4d468f0b1cf910112af2b2a32f7b Mon Sep 17 00:00:00 2001 From: olivierwilkinson Date: Thu, 21 Jan 2021 13:25:13 +0000 Subject: [PATCH] fix: prevent stale session and element references Fetching the document body when setupBrowser is called means the session id and element id stored on the body element can become stale. Fetch the body element when the query is invoked to prevent this. --- README.md | 15 +++++++-------- src/index.ts | 23 ++++++++++++++++------- test/configure.e2e.ts | 4 ++-- test/queries.e2e.ts | 40 ++++++++++++++++++++-------------------- test/setupBrowser.e2e.ts | 28 ++++++++++++++++++++-------- test/within.e2e.ts | 4 ++-- 6 files changed, 67 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 5e8678b..9aaa86b 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,15 @@ npm install --save-dev webdriverio-testing-library ### setupBrowser -Accepts a WebdriverIO BrowserObject and resolves with -dom-testing-library queries modifed to return WebdriverIO Elements. All the -queries are async, including queryBy and getBy variants, and are bound to -`document.body` by default. +Accepts a WebdriverIO BrowserObject and returns dom-testing-library queries +modifed to return WebdriverIO Elements. All the queries are async, including +queryBy and getBy variants, and are bound to `document.body` by default. ``` const {setupBrowser} = require('webdriverio-testing-library'); it('can click button', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) const button = await getByText('Button Text'); await button.click(); @@ -84,13 +83,13 @@ commands are scoped to the element. ``` it('adds queries as browser commands', async () => { - await setupBrowser(browser); + setupBrowser(browser); expect(await browser.getByText('Page Heading')).toBeDefined() }) it('adds queries as element commands scoped to element', async () => { - await setupBrowser(browser); + setupBrowser(browser); const nested = await browser.$('*[data-testid="nested"]'); const button = await nested.getByText('Button Text') @@ -132,7 +131,7 @@ afterEach(() => { }) it('lets you configure queries', async () => { - const {getByTestId} = await setupBrowser(browser) + const {getByTestId} = setupBrowser(browser) expect(await getByTestId('testid-in-data-automation-id-attr')).toBeDefined() }) diff --git a/src/index.ts b/src/index.ts index 8ca0b2e..0b4917c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,26 +125,35 @@ function within(element: Element) { ) as WebdriverIOQueries } -async function setupBrowser(browser: BrowserObject | MultiRemoteBrowserObject) { - const body = await browser.$('body') - const queries = within(body) +function setupBrowser(browser: BrowserObject | MultiRemoteBrowserObject) { + const queries: { [key: string]: any } = {}; + + Object.keys(baseQueries).forEach((key) => { + const queryName = key as keyof typeof baseQueries; + + const query = async (...args: any[]) => { + const body = await browser.$('body'); + return within(body)[queryName](...args); + } + + // add query to response queries + queries[queryName] = query; - Object.entries(queries).forEach(([queryName, query]) => { // add query to BrowserObject browser.addCommand(queryName, query) - // add query to scoped to Element + // add query to Elements browser.addCommand( queryName, function (...args: any[]) { const element = this as Element - return within(element)[queryName as keyof typeof queries](...args) + return within(element)[queryName](...args) }, true, ) }) - return queries + return queries as WebdriverIOQueries } function configure(config: Partial) { diff --git a/test/configure.e2e.ts b/test/configure.e2e.ts index 683f822..6ca648b 100644 --- a/test/configure.e2e.ts +++ b/test/configure.e2e.ts @@ -9,13 +9,13 @@ describe('configure', () => { }) it('supports alternative testIdAttribute', async () => { - const {getByTestId} = await setupBrowser(browser) + const {getByTestId} = setupBrowser(browser) expect(await getByTestId('image-with-random-alt-tag')).toBeDefined() }) it('works after navigation', async () => { - const {getByText, findByTestId} = await setupBrowser(browser) + const {getByText, findByTestId} = setupBrowser(browser) const goToPageTwoLink = await getByText('Go to Page 2') await goToPageTwoLink.click() diff --git a/test/queries.e2e.ts b/test/queries.e2e.ts index cddaa0e..e8e46d3 100644 --- a/test/queries.e2e.ts +++ b/test/queries.e2e.ts @@ -2,99 +2,99 @@ import {setupBrowser} from '../src'; describe('queries', () => { it('queryBy resolves with matching element', async () => { - const {queryByText} = await setupBrowser(browser) + const {queryByText} = setupBrowser(browser) const button = await queryByText('Unique Button Text') expect(await button?.getText()).toEqual('Unique Button Text') }) it('queryBy resolves with null when there are no matching elements', async () => { - const {queryByText} = await setupBrowser(browser) + const {queryByText} = setupBrowser(browser) const button = await queryByText('Text that does not exist') expect(button).toBeNull() }) it('getBy resolves with matching element', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) const button = await getByText('Unique Button Text') expect(await button.getText()).toEqual('Unique Button Text') }) it('getBy rejects when there are no matching elements', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) await expect(getByText('Text that does not exist')).rejects.toThrow() }) it('getBy rejects when there are multiple matching elements', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) await expect(getByText('Button Text')).rejects.toThrow() }) it('findBy waits for matching element and resolves with it', async () => { - const {findByText} = await setupBrowser(browser) + const {findByText} = setupBrowser(browser) const button = await findByText('Unique Delayed Button Text') expect(await button.getText()).toEqual('Unique Delayed Button Text') }) it('findBy rejects when there is no matching element after timeout', async () => { - const {findByText} = await setupBrowser(browser) + const {findByText} = setupBrowser(browser) await expect(findByText('Text that does not exist')).rejects.toThrow() }) it('findBy rejects when there are multiple matching elements', async () => { - const {findByText} = await setupBrowser(browser) + const {findByText} = setupBrowser(browser) await expect(findByText('Delayed Button Text')).rejects.toThrow() }) it('queryAllBy resolves with matching elements', async () => { - const {queryAllByText} = await setupBrowser(browser) + const {queryAllByText} = setupBrowser(browser) const chans = await queryAllByText('Button Text') expect(chans).toHaveLength(2) }) it('queryAllBy resolves with an empty array when there are no matching elements', async () => { - const {queryAllByText} = await setupBrowser(browser) + const {queryAllByText} = setupBrowser(browser) const chans = await queryAllByText('Text that does not exist') expect(chans).toHaveLength(0) }) it('getAllBy resolves matching elements', async () => { - const {getAllByText} = await setupBrowser(browser) + const {getAllByText} = setupBrowser(browser) const buttons = await getAllByText('Button Text') expect(buttons).toHaveLength(2) }) it('getAllBy rejects when there are no matching elements', async () => { - const {getAllByText} = await setupBrowser(browser) + const {getAllByText} = setupBrowser(browser) await expect(getAllByText('Text that does not exist')).rejects.toThrow() }) it('findAllBy waits for matching elements and resolves with them', async () => { - const {findAllByText} = await setupBrowser(browser) + const {findAllByText} = setupBrowser(browser) const buttons = await findAllByText('Delayed Button Text') expect(buttons).toHaveLength(2) }) it('findAllBy rejects when there are no matching elements after timeout', async () => { - const {findAllByText} = await setupBrowser(browser) + const {findAllByText} = setupBrowser(browser) await expect(findAllByText('Text that does not exist')).rejects.toThrow() }) it('can click resolved elements', async () => { - const {getByText, getAllByText} = await setupBrowser(browser) + const {getByText, getAllByText} = setupBrowser(browser) const uniqueButton = await getByText('Unique Button Text') const buttons = await getAllByText('Button Text') @@ -109,21 +109,21 @@ describe('queries', () => { }) it('support Regular Expressions', async () => { - const {getAllByText} = await setupBrowser(browser) + const {getAllByText} = setupBrowser(browser) const chans = await getAllByText(/Jackie Chan/) expect(chans).toHaveLength(2) }) it('support options', async () => { - const {getAllByText} = await setupBrowser(browser) + const {getAllByText} = setupBrowser(browser) const chans = await getAllByText('Jackie Chan', {exact: false}) expect(chans).toHaveLength(2) }) it('support waitFor options', async () => { - const {findByText} = await setupBrowser(browser) + const {findByText} = setupBrowser(browser) await expect( findByText('Unique Delayed Button Text', {}, {timeout: 0}), @@ -131,7 +131,7 @@ describe('queries', () => { }) it('support being passed undefined arguments', async () => { - const {findByText} = await setupBrowser(browser) + const {findByText} = setupBrowser(browser) const button = await findByText( 'Unique Delayed Button Text', @@ -142,7 +142,7 @@ describe('queries', () => { }) it('retains error messages', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) await expect(getByText('Text that does not exist')).rejects.toThrowError( /Unable to find an element with the text/, diff --git a/test/setupBrowser.e2e.ts b/test/setupBrowser.e2e.ts index 8b2c1b7..b3ccf60 100644 --- a/test/setupBrowser.e2e.ts +++ b/test/setupBrowser.e2e.ts @@ -1,7 +1,8 @@ +import path from 'path' import {queries as baseQueries} from '@testing-library/dom' import {setupBrowser} from '../src' -import { WebdriverIOQueries } from '../src/types' +import {WebdriverIOQueries} from '../src/types' declare global { namespace WebdriverIO { @@ -11,8 +12,8 @@ declare global { } describe('setupBrowser', () => { - it('resolves with all queries', async () => { - const queries = await setupBrowser(browser) + it('resolves with all queries', () => { + const queries = setupBrowser(browser) const queryNames = Object.keys(queries) Object.keys(baseQueries).forEach((queryName) => @@ -21,13 +22,13 @@ describe('setupBrowser', () => { }) it('binds queries to document body', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) expect(await getByText('Page Heading')).toBeDefined() }) it('still works after page navigation', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) const goToPageTwoLink = await getByText('Go to Page 2') await goToPageTwoLink.click() @@ -36,21 +37,32 @@ describe('setupBrowser', () => { }) it('still works after refresh', async () => { - const {getByText} = await setupBrowser(browser) + const {getByText} = setupBrowser(browser) await browser.refresh() expect(await getByText('Page Heading')).toBeDefined() }) + it('still works after session reload', async () => { + const {getByText} = setupBrowser(browser) + + await browser.reloadSession() + await browser.url( + `file:///${path.join(__dirname, '../test-app/index.html')}`, + ) + + expect(await getByText('Page Heading')).toBeDefined() + }) + it('adds queries as browser commands', async () => { - await setupBrowser(browser) + setupBrowser(browser) expect(await browser.getByText('Page Heading')).toBeDefined() }) it('adds queries as element commands scoped to element', async () => { - await setupBrowser(browser) + setupBrowser(browser) const nested = await browser.$('*[data-testid="nested"]') const button = await nested.getByText('Button Text') diff --git a/test/within.e2e.ts b/test/within.e2e.ts index de490e9..cd177aa 100644 --- a/test/within.e2e.ts +++ b/test/within.e2e.ts @@ -11,7 +11,7 @@ describe('within', () => { }) it('works with elements from GetBy query', async () => { - const {getByTestId} = await setupBrowser(browser) + const {getByTestId} = setupBrowser(browser) const nested = await getByTestId('nested') const button = await within(nested).getByText('Button Text') @@ -21,7 +21,7 @@ describe('within', () => { }) it('works with elements from AllBy query', async () => { - const {getAllByTestId} = await setupBrowser(browser) + const {getAllByTestId} = setupBrowser(browser) const nestedDivs = await getAllByTestId(/nested/) expect(nestedDivs).toHaveLength(2)