import path from 'node:path'
import type { Mock } from 'vitest'
import { expect, describe, it, vi, beforeAll, beforeEach } from 'vitest'
import { remote } from '../../../src/index.js'
vi.mock('fetch')
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
describe('scrollIntoView test', () => {
let browser: WebdriverIO.Browser
let elem: WebdriverIO.Element
const defaultIOSSelector = '-ios predicate string:type == "XCUIElementTypeApplication"'
const defaultAndroidSelector = '//android.widget.ScrollView'
beforeEach(() => {
vi.mocked(fetch).mockClear()
})
describe('desktop', () => {
beforeAll(async () => {
browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
browserName: 'foobar',
},
})
elem = await browser.$('#foo')
vi.spyOn(browser, 'getWindowSize').mockResolvedValue({ height: 800, width: 600 })
vi.spyOn(browser, 'getElementRect').mockImplementation(() =>
elem.getElementRect(elem.elementId).catch(() =>
({ x: 15, y: 20, height: 30, width: 50 })
)
)
})
it('scrolls by default the element to the top', async () => {
await elem.scrollIntoView()
const optionsVoid = vi.mocked(fetch).mock.calls.slice(-2, -1)[0][1] as any
expect(JSON.parse(optionsVoid.body)).toMatchSnapshot()
})
it('scrolls element when using boolean scroll options', async () => {
await elem.scrollIntoView(true)
const optionsTrue = vi.mocked(fetch).mock.calls.slice(-2, -1)[0][1] as any
expect(JSON.parse(optionsTrue.body)).toMatchSnapshot()
vi.mocked(fetch).mockClear()
await elem.scrollIntoView(false)
const optionsFalse = vi.mocked(fetch).mock.calls.slice(-2, -1)[0][1] as any
expect(JSON.parse(optionsFalse.body)).toMatchSnapshot()
})
it('scrolls element using scroll into view options', async () => {
await elem.scrollIntoView({ block: 'center', inline: 'center' })
const optionsCenter = vi.mocked(fetch).mock.calls.slice(-2, -1)[0][1] as any
expect(JSON.parse(optionsCenter.body)).toMatchSnapshot()
})
it('falls back using Web API if scroll action fails', async () => {
vi.mocked(fetch).customResponseFor(/\/actions/, { error: 'invalid parameter' })
elem.elementId = { scrollIntoView: 'mockFunction' }
await elem.scrollIntoView({})
expect(vi.mocked(fetch).mock.calls.pop()![0]!.href.endsWith('/execute/sync'))
.toBe(true)
})
it('rounds float delta values', async () => {
vi.spyOn(browser, 'getWindowSize').mockResolvedValue({ height: 800.123, width: 600.321 })
vi.spyOn(browser, 'getElementRect').mockResolvedValue(
({ x: 15.34, y: 20.23, height: 30.2344, width: 50.543 }))
await elem.scrollIntoView({ block: 'center', inline: 'center' })
const optionsCenter = vi.mocked(fetch).mock.calls.slice(-2, -1)[0][1] as any
expect(JSON.parse(optionsCenter.body).actions[0].actions[0].deltaX).toBe(0)
expect(JSON.parse(optionsCenter.body).actions[0].actions[0].deltaY).toBe(0)
expect(JSON.parse(optionsCenter.body).actions[0].actions[0].y).toBe(-385)
})
})
describe('mobile web', () => {
beforeAll(async () => {
browser = await remote({
baseUrl: 'http://foobar.com',
capabilities: {
browserName: 'foobar',
mobileMode: true
} as any
})
elem = await browser.$('#foo')
elem.elementId = { scrollIntoView: 'mockFunction' }
})
beforeEach(() => {
vi.mocked(fetch).mockClear()
})
it('scrolls by default the element to the top', async () => {
await elem.scrollIntoView()
const { calls } = vi.mocked(fetch).mock
expect(calls).toHaveLength(1)
const [[executeCallUrl, executeCallOptions]] = calls as any
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
expect(JSON.parse(executeCallOptions.body).script).toEqual(
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
expect(JSON.parse(executeCallOptions.body).args).toHaveLength(2)
expect(JSON.parse(executeCallOptions.body).args[1]).toEqual({ block: 'start', inline: 'nearest' })
})
it('scrolls element when using boolean scroll options', async () => {
await elem.scrollIntoView(true)
const { calls } = vi.mocked(fetch).mock
expect(calls).toHaveLength(1)
const [[executeCallUrl, executeCallOptions]] = calls as any
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
expect(JSON.parse(executeCallOptions.body).script).toEqual(
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
expect(JSON.parse(executeCallOptions.body).args).toHaveLength(2)
expect(JSON.parse(executeCallOptions.body).args[1]).toEqual(true)
})
it('scrolls element using scroll into view options', async () => {
await elem.scrollIntoView({ block: 'end', inline: 'center' })
const { calls } = vi.mocked(fetch).mock
expect(calls).toHaveLength(1)
const [[executeCallUrl, executeCallOptions]] = calls as any
expect(executeCallUrl.pathname).toEqual('/session/foobar-123/execute/sync')
expect(JSON.parse(executeCallOptions.body).script).toEqual(
expect.stringContaining('return ((elem, options2) => elem.scrollIntoView(options2)).apply(null, arguments)'))
expect(JSON.parse(executeCallOptions.body).args).toHaveLength(2)
expect(JSON.parse(executeCallOptions.body).args[1]).toEqual({ block: 'end', inline: 'center' })
})
})
describe('mobile native app', () => {
beforeAll(async () => {
browser = await remote({
capabilities: {
mobileMode: true,
nativeAppMode: true,
platformName: 'iOS',
platformVersion: '16',
deviceName: 'iphone',
app: 'foo.ipa',
} as any
})
elem = await browser.$('#foo')
elem.elementId = { scrollIntoView: 'mockFunction' }
})
beforeEach(() => {
vi.mocked(fetch).mockClear()
vi.spyOn(browser, 'pause').mockResolvedValue(undefined)
vi.spyOn(browser, '$$')
})
it('no scroll is needed', async () => {
(browser.$$ as Mock).mockResolvedValue([{
elementId: 'scrollable-element-id',
isDisplayed: vi.fn().mockResolvedValue(true),
}])
vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(true)
await elem.scrollIntoView()
const { calls } = vi.mocked(fetch).mock
expect(calls).toHaveLength(0)
})
it('scrolls by default the element to the top', async () => {
(browser.$$ as Mock).mockResolvedValue([{
elementId: 'scrollable-element-id',
isDisplayed: vi.fn().mockResolvedValue(true),
}])
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
await elem.scrollIntoView()
expect(isDisplayedSpy).toBeCalledTimes(2)
expect(swipeSpy).toBeCalledTimes(1)
expect(swipeSpy).toBeCalledWith({ direction: 'up' })
swipeSpy.mockRestore()
isDisplayedSpy.mockRestore()
})
it('scrolls to the left with some options', async () => {
(browser.$$ as Mock).mockResolvedValue([{
elementId: 'scrollable-element-id',
isDisplayed: vi.fn().mockResolvedValue(true),
}])
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(false).mockResolvedValueOnce(true)
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
await elem.scrollIntoView({
direction: 'left',
duration: 3000,
percent: 0.5,
})
expect(isDisplayedSpy).toBeCalledTimes(2)
expect(swipeSpy).toBeCalledTimes(1)
expect(swipeSpy).toBeCalledWith({ direction: 'left', duration: 3000, percent: 0.5 })
swipeSpy.mockRestore()
isDisplayedSpy.mockRestore()
})
it('does not find the element after max 5 scrolls', async () => {
(browser.$$ as Mock).mockResolvedValue([{
elementId: 'scrollable-element-id',
isDisplayed: vi.fn().mockResolvedValue(true),
}])
const isDisplayedSpy = vi.spyOn(elem, 'isDisplayed').mockResolvedValue(false)
const swipeSpy = vi.spyOn(browser, 'swipe').mockResolvedValue(undefined)
try {
await elem.scrollIntoView({ maxScrolls: 5 })
} catch (err: any) {
expect(isDisplayedSpy).toBeCalledTimes(5)
expect(swipeSpy).toBeCalledTimes(5)
expect(err.message).toMatchSnapshot()
}
})
})
})