Fix broken BrowserInterface type (#66461)

This commit is contained in:
Hendrik Liebau 2024-06-03 14:56:49 +02:00 committed by GitHub
parent 48e9cd9b60
commit 994d8ee2c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 112 additions and 133 deletions

View file

@ -1,4 +1,3 @@
import { type BrowserInterface } from 'next-webdriver'
import { nextTestSetup } from 'e2e-utils'
import { check } from 'next-test-utils'
@ -8,7 +7,7 @@ describe('Strict Mode enabled by default', () => {
})
// TODO: modern StrictMode does not double invoke effects during hydration: https://github.com/facebook/react/pull/28951
it.skip('should work using browser', async () => {
const browser: BrowserInterface = await next.browser('/')
const browser = await next.browser('/')
await check(async () => {
const text = await browser.elementByCss('p').text()
// FIXME: Bug in React. Strict Effects no longer work in current beta.

View file

@ -48,7 +48,7 @@ describe('useDefineForClassFields SWC option', () => {
let data_foundLog = false
let name_foundLog = false
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
browserLogs.forEach((log) => {
if (log.message.includes('data changed')) {

View file

@ -85,7 +85,7 @@ describe.each([[''], ['/docs']])(
let browser
try {
browser = await webdriver(next.url, path)
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
browserLogs.forEach((log) => {
if (log.message.includes('Next.js auto-prefetches automatically')) {

View file

@ -119,7 +119,7 @@ describe('next/dynamic', () => {
)
if ((global as any).browserName === 'chrome') {
const logs = await browser.log('browser')
const logs = await browser.log()
logs.forEach((logItem) => {
expect(logItem.message).not.toMatch(

View file

@ -12,7 +12,7 @@ describe('styled-components SWC transform', () => {
async function matchLogs$(browser) {
let foundLog = false
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
browserLogs.forEach((log) => {
if (log.message.includes('Warning: Prop `%s` did not match.')) {

View file

@ -565,7 +565,7 @@ describe('Client Navigation', () => {
describe('check hydration mis-match', () => {
it('should not have hydration mis-match for hash link', async () => {
const browser = await webdriver(next.appPort, '/nav/hash-changes')
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let found = false
browserLogs.forEach((log) => {
console.log('log.message', log.message)
@ -1717,7 +1717,7 @@ describe.each([[false], [true]])(
await browser.waitForElementByCss('h1')
await waitFor(1000)
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let foundStyles = false
let foundScripts = false
const logs = []
@ -1750,7 +1750,7 @@ describe.each([[false], [true]])(
browser = await webdriver(next.appPort, '/head')
await browser.waitForElementByCss('h1')
await waitFor(1000)
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let found = false
browserLogs.forEach((log) => {
if (log.message.includes('Use next/script instead')) {
@ -1771,7 +1771,7 @@ describe.each([[false], [true]])(
browser = await webdriver(next.appPort, '/head-with-json-ld-snippet')
await browser.waitForElementByCss('h1')
await waitFor(1000)
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let found = false
browserLogs.forEach((log) => {
if (log.message.includes('Use next/script instead')) {

View file

@ -36,7 +36,7 @@ describe('router autoscrolling on navigation with css modules', () => {
describe('vertical scroll when page imports css modules', () => {
it('should scroll to top of document when navigating between to pages without layout when', async () => {
const browser: BrowserInterface = await next.browser('/1')
const browser = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)
@ -46,7 +46,7 @@ describe('router autoscrolling on navigation with css modules', () => {
})
it('should scroll when clicking in JS', async () => {
const browser: BrowserInterface = await next.browser('/1')
const browser = await next.browser('/1')
await scrollTo(browser, { x: 0, y: 1000 })
expect(await getTopScroll(browser)).toBe(1000)

View file

@ -8,7 +8,6 @@ import * as fs from 'fs'
import * as path from 'path'
import * as os from 'os'
import * as Log from './utils/log'
import { BrowserInterface } from '../../../lib/next-webdriver'
const runtimes = ['nodejs', 'edge']
@ -257,7 +256,7 @@ describe.each(runtimes)('unstable_after() in %s runtime', (runtimeValue) => {
const EXPECTED_ERROR =
/An error occurred in a function passed to `unstable_after\(\)`: .+?: Cookies can only be modified in a Server Action or Route Handler\./
const browser: BrowserInterface = await next.browser('/123/setting-cookies')
const browser = await next.browser('/123/setting-cookies')
// after() from render
expect(next.cliOutput).toMatch(EXPECTED_ERROR)

View file

@ -16,7 +16,7 @@ describe('root-layout-redirect', () => {
.text()
).toBe('Result Page')
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let foundErrors = false

View file

@ -9,7 +9,10 @@ describe('app dir - search params keys', () => {
it('should keep the React router instance the same when changing the search params', async () => {
const browser = await next.browser('/')
const searchParams = browser.waitForElementByCss('#search-params').text()
const searchParams = await browser
.waitForElementByCss('#search-params')
.text()
await browser.elementByCss('#increment').click()
await browser.elementByCss('#increment').click()

View file

@ -8,7 +8,7 @@ import path from 'path'
async function matchLogs(browser, includes: string) {
let found = false
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
browserLogs.forEach((log) => {
if (log.message.includes(includes)) {

View file

@ -6,7 +6,7 @@ export default (context, render) => {
it('should load inline script by hash', async () => {
const browser = await webdriver(context.appPort, '/?withCSP=hash')
if (global.browserName === 'chrome') {
const errLog = await browser.log('browser')
const errLog = await browser.log()
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
}
await browser.close()
@ -15,7 +15,7 @@ export default (context, render) => {
it('should load inline script by nonce', async () => {
const browser = await webdriver(context.appPort, '/?withCSP=nonce')
if (global.browserName === 'chrome') {
const errLog = await browser.log('browser')
const errLog = await browser.log()
expect(errLog.filter((e) => e.source === 'security')).toEqual([])
}
await browser.close()

View file

@ -238,7 +238,7 @@ function runTests(mode) {
await browser.elementById('belowthefold').getAttribute('loading')
).toBe(null)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toMatch(
@ -381,7 +381,7 @@ function runTests(mode) {
)
if (mode === 'dev') {
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toMatch(
@ -657,7 +657,7 @@ function runTests(mode) {
)
if (mode === 'dev') {
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toMatch(
@ -1064,7 +1064,7 @@ function runTests(mode) {
return 'done'
}, 'done')
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(await hasRedbox(browser)).toBe(false)
@ -1353,7 +1353,7 @@ function runTests(mode) {
if (mode === 'dev') {
it('should not log incorrect warnings', async () => {
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toMatch(/Image with src (.*) has "fill"/gm)
@ -1364,7 +1364,7 @@ function runTests(mode) {
it('should log warnings when using fill mode incorrectly', async () => {
browser = await webdriver(appPort, '/fill-warnings')
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toContain(
@ -1383,7 +1383,7 @@ function runTests(mode) {
it('should not log warnings when image unmounts', async () => {
browser = await webdriver(appPort, '/should-not-warn-unmount')
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toContain(

View file

@ -133,9 +133,7 @@ function runTests(mode) {
expect(await hasRedbox(browser)).toBe(false)
await check(async () => {
return (await browser.log('browser'))
.map((log) => log.message)
.join('\n')
return (await browser.log()).map((log) => log.message).join('\n')
}, /Image is missing required "src" property/gm)
})

View file

@ -239,7 +239,7 @@ function runTests(mode) {
await browser.elementById('belowthefold').getAttribute('loading')
).toBe(null)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toMatch(
@ -382,7 +382,7 @@ function runTests(mode) {
)
if (mode === 'dev') {
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toMatch(
@ -658,7 +658,7 @@ function runTests(mode) {
)
if (mode === 'dev') {
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toMatch(
@ -1355,7 +1355,7 @@ function runTests(mode) {
if (mode === 'dev') {
it('should not log incorrect warnings', async () => {
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toMatch(/Image with src (.*) has "fill"/gm)
@ -1366,7 +1366,7 @@ function runTests(mode) {
it('should log warnings when using fill mode incorrectly', async () => {
browser = await webdriver(appPort, '/fill-warnings')
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).toContain(
@ -1385,7 +1385,7 @@ function runTests(mode) {
it('should not log warnings when image unmounts', async () => {
browser = await webdriver(appPort, '/should-not-warn-unmount')
await waitFor(1000)
const warnings = (await browser.log('browser'))
const warnings = (await browser.log())
.map((log) => log.message)
.join('\n')
expect(warnings).not.toContain(

View file

@ -61,7 +61,7 @@ const runTests = (isDev) => {
await browser.waitForElementByCss('#onload-div')
await waitFor(1000)
const logs = await browser.log('browser')
const logs = await browser.log()
const filteredLogs = logs.filter(
(log) =>
!log.message.includes('Failed to load resource') &&

View file

@ -5,21 +5,18 @@ export type Event = 'request'
* classes should build on, it is the bare
* methods we aim to support across tests
*/
export abstract class BrowserInterface implements PromiseLike<any> {
private promise?: Promise<any>
then: Promise<any>['then']
catch: Promise<any>['catch']
finally: Promise<any>['finally'];
export abstract class BrowserInterface<TCurrent = any> {
private promise?: Promise<TCurrent>;
// necessary for the type of the function below
readonly [Symbol.toStringTag]: string = 'BrowserInterface'
protected chain<T>(
nextCall: (current: any) => T | PromiseLike<T>
): BrowserInterface & Promise<T> {
protected chain<TNext>(
nextCall: (current: TCurrent) => TNext | Promise<TNext>
): BrowserInterface<TNext> & Promise<TNext> {
const promise = Promise.resolve(this.promise).then(nextCall)
function get(target: BrowserInterface, p: string | symbol): any {
function get(target: BrowserInterface<TNext>, p: string | symbol): any {
switch (p) {
case 'promise':
return promise
@ -39,12 +36,6 @@ export abstract class BrowserInterface implements PromiseLike<any> {
})
}
protected chainWithReturnValue<T>(
callback: (value: any) => T | PromiseLike<T>
): Promise<T> {
return Promise.resolve(this.promise).then(callback)
}
abstract setup(
browserName: string,
locale: string,
@ -54,36 +45,43 @@ export abstract class BrowserInterface implements PromiseLike<any> {
): Promise<void>
abstract close(): Promise<void>
abstract elementsByCss(selector: string): BrowserInterface[]
abstract elementByCss(selector: string): BrowserInterface
abstract elementById(selector: string): BrowserInterface
abstract touchStart(): BrowserInterface
abstract click(opts?: { modifierKey?: boolean }): BrowserInterface
abstract keydown(key: string): BrowserInterface
abstract keyup(key: string): BrowserInterface
abstract type(text: string): BrowserInterface
abstract moveTo(): BrowserInterface
// TODO(NEXT-290): type this correctly as awaitable
abstract elementsByCss(
selector: string
): BrowserInterface<any[]> & Promise<any[]>
abstract elementByCss(selector: string): BrowserInterface<any> & Promise<any>
abstract elementById(selector: string): BrowserInterface<any> & Promise<any>
abstract touchStart(): BrowserInterface<any> & Promise<any>
abstract click(): BrowserInterface<any> & Promise<any>
abstract keydown(key: string): BrowserInterface<any> & Promise<any>
abstract keyup(key: string): BrowserInterface<any> & Promise<any>
abstract type(text: string): BrowserInterface<any> & Promise<any>
abstract moveTo(): BrowserInterface<any> & Promise<any>
abstract waitForElementByCss(
selector: string,
timeout?: number
): BrowserInterface
abstract waitForCondition(snippet: string, timeout?: number): BrowserInterface
): BrowserInterface<any> & Promise<any>
abstract waitForCondition(
snippet: string,
timeout?: number
): BrowserInterface<any> & Promise<any>
/**
* Use browsers `go back` functionality.
*/
abstract back(options?: any): BrowserInterface
abstract back(options?: any): BrowserInterface<any> & Promise<any>
/**
* Use browsers `go forward` functionality. Inverse of back.
*/
abstract forward(options?: any): BrowserInterface
abstract refresh(): BrowserInterface
abstract forward(options?: any): BrowserInterface<any> & Promise<any>
abstract refresh(): BrowserInterface<any> & Promise<any>
abstract setDimensions(opts: {
height: number
width: number
}): BrowserInterface
abstract addCookie(opts: { name: string; value: string }): BrowserInterface
abstract deleteCookies(): BrowserInterface
}): BrowserInterface<any> & Promise<any>
abstract addCookie(opts: {
name: string
value: string
}): BrowserInterface<any> & Promise<any>
abstract deleteCookies(): BrowserInterface<void> & Promise<void>
abstract on(event: Event, cb: (...args: any[]) => void): void
abstract off(event: Event, cb: (...args: any[]) => void): void
abstract loadPage(
@ -93,7 +91,7 @@ export abstract class BrowserInterface implements PromiseLike<any> {
cpuThrottleRate,
beforePageLoad,
pushErrorAsConsoleLog,
}: {
}?: {
disableCache?: boolean
cpuThrottleRate?: number
beforePageLoad?: Function
@ -101,20 +99,14 @@ export abstract class BrowserInterface implements PromiseLike<any> {
}
): Promise<void>
abstract get(url: string): Promise<void>
abstract getValue<T = any>(): Promise<T>
abstract getAttribute<T = any>(name: string): Promise<T>
abstract eval<T = any>(snippet: string | Function, ...args: any[]): Promise<T>
abstract evalAsync<T = any>(
snippet: string | Function,
...args: any[]
): Promise<T>
abstract getValue(): Promise<string>
abstract getAttribute(name: string): Promise<string>
abstract eval(snippet: string | Function, ...args: any[]): Promise<any>
abstract evalAsync(snippet: string | Function, ...args: any[]): Promise<any>
abstract text(): Promise<string>
abstract getComputedCss(prop: string): Promise<string>
abstract hasElementByCssSelector(selector: string): Promise<boolean>
abstract log(): Promise<
{ source: 'error' | 'info' | 'log'; message: string }[]
>
abstract log(): Promise<{ source: string; message: string }[]>
abstract websocketFrames(): Promise<any[]>
abstract url(): Promise<string>
abstract waitForIdleNetwork(): Promise<void>

View file

@ -280,31 +280,25 @@ export class Playwright extends BrowserInterface {
await page.goto(url, { waitUntil: 'load' })
}
back(options): BrowserInterface {
back(options) {
return this.chain(async () => {
await page.goBack(options)
})
}
forward(options): BrowserInterface {
forward(options) {
return this.chain(async () => {
await page.goForward(options)
})
}
refresh(): BrowserInterface {
refresh() {
return this.chain(async () => {
await page.reload()
})
}
setDimensions({
width,
height,
}: {
height: number
width: number
}): BrowserInterface {
setDimensions({ width, height }: { height: number; width: number }) {
return this.chain(() => page.setViewportSize({ width, height }))
}
addCookie(opts: { name: string; value: string }): BrowserInterface {
addCookie(opts: { name: string; value: string }) {
return this.chain(async () =>
context.addCookies([
{
@ -315,7 +309,7 @@ export class Playwright extends BrowserInterface {
])
)
}
deleteCookies(): BrowserInterface {
deleteCookies() {
return this.chain(async () => context.clearCookies())
}
@ -373,21 +367,21 @@ export class Playwright extends BrowserInterface {
}) as any
}
async getAttribute<T = any>(attr) {
return this.chain((el: ElementHandleExt) => el.getAttribute(attr)) as T
async getAttribute(attr) {
return this.chain((el: ElementHandleExt) => el.getAttribute(attr))
}
hasElementByCssSelector(selector: string) {
return this.eval<boolean>(`!!document.querySelector('${selector}')`)
}
keydown(key: string): BrowserInterface {
keydown(key: string) {
return this.chain((el: ElementHandleExt) => {
return page.keyboard.down(key).then(() => el)
})
}
keyup(key: string): BrowserInterface {
keyup(key: string) {
return this.chain((el: ElementHandleExt) => {
return page.keyboard.up(key).then(() => el)
})
@ -418,7 +412,7 @@ export class Playwright extends BrowserInterface {
return el
})
})
) as any as BrowserInterface[]
)
}
waitForElementByCss(selector, timeout?: number) {
@ -441,7 +435,7 @@ export class Playwright extends BrowserInterface {
}
eval<T = any>(fn: any, ...args: any[]): Promise<T> {
return this.chainWithReturnValue(() =>
return this.chain(() =>
page
.evaluate(fn, ...args)
.catch((err) => {
@ -455,7 +449,7 @@ export class Playwright extends BrowserInterface {
)
}
async evalAsync<T = any>(fn: any, ...args: any[]) {
async evalAsync<T = any>(fn: any) {
if (typeof fn === 'function') {
fn = fn.toString()
}
@ -477,15 +471,15 @@ export class Playwright extends BrowserInterface {
}
async log() {
return this.chain(() => pageLogs) as any
return this.chain(() => pageLogs)
}
async websocketFrames() {
return this.chain(() => websocketFrames) as any
return this.chain(() => websocketFrames)
}
async url() {
return this.chain(() => page.evaluate('window.location.href')) as any
return this.chain(() => page.url())
}
async waitForIdleNetwork(): Promise<void> {

View file

@ -706,9 +706,7 @@ describe('Production Usage', () => {
// @ts-expect-error Exists on window
window.__DATA_BE_GONE = 'true'
})
await browser
.waitForElementByCss('#to-nonexistent-page')
.click('#to-nonexistent-page')
await browser.waitForElementByCss('#to-nonexistent-page').click()
await browser.waitForElementByCss('.about-page')
const oldData = await browser.eval(`window.__DATA_BE_GONE`)
@ -1082,7 +1080,7 @@ describe('Production Usage', () => {
let browser
try {
browser = await webdriver(next.appPort, '/development-logs')
const browserLogs = await browser.log('browser')
const browserLogs = await browser.log()
let found = false
browserLogs.forEach((log) => {
if (log.message.includes('Next.js auto-prefetches automatically')) {

View file

@ -1,23 +0,0 @@
{
"compilerOptions": {
"strict": false,
"noEmit": true,
"allowJs": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"module": "esnext",
"target": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"types": ["react", "jest", "node", "trusted-types", "jest-extended"],
"paths": {
"development-sandbox": ["./test/lib/development-sandbox"],
"next-test-utils": ["./test/lib/next-test-utils"],
"amp-test-utils": ["./test/lib/amp-test-utils"],
"next-webdriver": ["./test/lib/next-webdriver"],
"e2e-utils": ["./test/lib/e2e-utils"],
"test-data-service/*": ["./test/lib/test-data-service/*"],
"test-log": ["./test/lib/test-log"]
}
}
}

View file

@ -1,5 +1,24 @@
{
"extends": "./tsconfig.base.json",
"include": ["test/**/*.test.ts", "test/**/*.test.tsx"],
"exclude": ["node_modules"]
"compilerOptions": {
"strict": false,
"noEmit": true,
"allowJs": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"module": "esnext",
"target": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"types": ["react", "jest", "node", "trusted-types", "jest-extended"],
"paths": {
"development-sandbox": ["./test/lib/development-sandbox"],
"next-test-utils": ["./test/lib/next-test-utils"],
"amp-test-utils": ["./test/lib/amp-test-utils"],
"next-webdriver": ["./test/lib/next-webdriver"],
"e2e-utils": ["./test/lib/e2e-utils"],
"test-data-service/*": ["./test/lib/test-data-service/*"],
"test-log": ["./test/lib/test-log"]
}
},
"include": ["test/**/*.test.ts", "test/**/*.test.tsx", "test/lib/**/*.ts"]
}