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', }, }) // @ts-expect-error elem = await browser.$('#foo') vi.spyOn(browser, 'getWindowSize').mockResolvedValue({ height: 800, width: 600 }) vi.spyOn(browser, 'getElementRect').mockImplementation(() => elem.getElementRect(elem.elementId).catch(() => // there is a test forcing `elementId` to be invalid to check the fallback to the web API ({ 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 () => { // @ts-expect-error mock feature vi.mocked(fetch).customResponseFor(/\/actions/, { error: 'invalid parameter' }) // @ts-expect-error mock feature elem.elementId = { scrollIntoView: 'mockFunction' } await elem.scrollIntoView({}) // @ts-expect-error mock implementation 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 }) // @ts-expect-error elem = await browser.$('#foo') // @ts-expect-error mock feature 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 }) // @ts-expect-error elem = await browser.$('#foo') // @ts-expect-error mock feature elem.elementId = { scrollIntoView: 'mockFunction' } }) beforeEach(() => { vi.mocked(fetch).mockClear() vi.spyOn(browser, 'pause').mockResolvedValue(undefined) vi.spyOn(browser, '$$') }) it('no scroll is needed', async () => { // Set some spy values (browser.$$ as Mock).mockResolvedValue([{ elementId: 'scrollable-element-id', isDisplayed: vi.fn().mockResolvedValue(true), }]) vi.spyOn(elem, 'isDisplayed').mockResolvedValueOnce(true) await elem.scrollIntoView() // call the mobileScrollUntilVisible but no scroll is needed const { calls } = vi.mocked(fetch).mock expect(calls).toHaveLength(0) }) it('scrolls by default the element to the top', async () => { // Set some spy values (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() // call the mobileScrollUntilVisible 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 () => { // Set some spy values (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, }) // call the mobileScrollUntilVisible 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 () => { // Set some spy values (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) { // call the mobileScrollUntilVisible 5 times for the max scrolls expect(isDisplayedSpy).toBeCalledTimes(5) expect(swipeSpy).toBeCalledTimes(5) expect(err.message).toMatchSnapshot() } }) }) })