Skip to content

Commit

Permalink
fix: prevent stale session and element references
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
olivierwilkinson committed Jan 21, 2021
1 parent d472f12 commit 993f019
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 47 deletions.
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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')
Expand Down Expand Up @@ -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()
})
Expand Down
23 changes: 16 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config>) {
Expand Down
4 changes: 2 additions & 2 deletions test/configure.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
40 changes: 20 additions & 20 deletions test/queries.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -109,29 +109,29 @@ 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}),
).rejects.toThrow()
})

it('support being passed undefined arguments', async () => {
const {findByText} = await setupBrowser(browser)
const {findByText} = setupBrowser(browser)

const button = await findByText(
'Unique Delayed Button Text',
Expand All @@ -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/,
Expand Down
28 changes: 20 additions & 8 deletions test/setupBrowser.e2e.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) =>
Expand All @@ -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()
Expand All @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions test/within.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)
Expand Down

0 comments on commit 993f019

Please sign in to comment.