refactor(tests): make chain more "correct" (#51728)

### Why?

I really dislike the way `.chain` works right now, it shouldn't mutate
the `BrowserInterface`, this PR changes it so it's just a pure chain
without weird side effects.

One example with the current version (before this PR):
```
const el = browser.elementByCss('#version-2')
await el.text()
// throws
await el.text()
```

### Additional Changes

- removes selenium (which is completely unused)
- updates playwright
- makes the playwright tracing not error all the time
This commit is contained in:
Leah 2024-02-14 20:14:24 +01:00 committed by GitHub
parent f336bd6ada
commit 60f0837b67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 207 additions and 689 deletions

View file

@ -185,8 +185,8 @@
"open": "9.0.0",
"outdent": "0.8.0",
"pixrem": "5.0.0",
"playwright-chromium": "1.35.1",
"playwright-core": "1.35.1",
"playwright": "1.41.2",
"playwright-chromium": "1.41.2",
"postcss": "8.4.31",
"postcss-nested": "4.2.1",
"postcss-pseudoelements": "5.0.0",

View file

@ -150,7 +150,7 @@
"@next/react-refresh-utils": "14.1.1-canary.52",
"@next/swc": "14.1.1-canary.52",
"@opentelemetry/api": "1.6.0",
"@playwright/test": "^1.35.1",
"@playwright/test": "1.41.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@types/amphtml-validator": "1.0.0",
@ -309,7 +309,7 @@
"vm-browserify": "1.1.2",
"watchpack": "2.4.0",
"web-vitals": "3.0.0",
"webpack": "5.86.0",
"webpack": "5.90.0",
"webpack-sources1": "npm:webpack-sources@1.4.3",
"webpack-sources3": "npm:webpack-sources@3.2.3",
"ws": "8.2.3",

View file

@ -401,12 +401,12 @@ importers:
pixrem:
specifier: 5.0.0
version: 5.0.0
playwright:
specifier: 1.41.2
version: 1.41.2
playwright-chromium:
specifier: 1.35.1
version: 1.35.1
playwright-core:
specifier: 1.35.1
version: 1.35.1
specifier: 1.41.2
version: 1.41.2
postcss:
specifier: 8.4.31
version: 8.4.31
@ -948,8 +948,8 @@ importers:
specifier: 1.6.0
version: 1.6.0
'@playwright/test':
specifier: ^1.35.1
version: 1.35.1
specifier: 1.41.2
version: 1.41.2
'@taskr/clear':
specifier: 1.1.0
version: 1.1.0
@ -5950,15 +5950,12 @@ packages:
- utf-8-validate
dev: true
/@playwright/test@1.35.1:
resolution: {integrity: sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==}
/@playwright/test@1.41.2:
resolution: {integrity: sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==}
engines: {node: '>=16'}
hasBin: true
dependencies:
'@types/node': 20.2.5
playwright-core: 1.35.1
optionalDependencies:
fsevents: 2.3.2
playwright: 1.41.2
dev: true
/@polka/url@1.0.0-next.11:
@ -19421,13 +19418,13 @@ packages:
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
dev: true
/playwright-chromium@1.35.1:
resolution: {integrity: sha512-bPuWIk/DYWtrg10ajcc9cZLo5s9XQrs6JF6wwiieqZ73rCG3PLVEm0RX8fCUzziuEAhYsoL09UNuKSKv7pKz9A==}
/playwright-chromium@1.41.2:
resolution: {integrity: sha512-1XoW4aGGRbS2BJLldtLcv2QW3deMv8myE5iCtfGRPq99BWqmBLJvJTgY/SyfBCoklwQvl91zUWYWHjCAuvKGkw==}
engines: {node: '>=16'}
hasBin: true
requiresBuild: true
dependencies:
playwright-core: 1.35.1
playwright-core: 1.41.2
dev: true
/playwright-core@1.19.2:
@ -19457,12 +19454,22 @@ packages:
- utf-8-validate
dev: true
/playwright-core@1.35.1:
resolution: {integrity: sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==}
/playwright-core@1.41.2:
resolution: {integrity: sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==}
engines: {node: '>=16'}
hasBin: true
dev: true
/playwright@1.41.2:
resolution: {integrity: sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.41.2
optionalDependencies:
fsevents: 2.3.2
dev: true
/please-upgrade-node@3.2.0:
resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==}
dependencies:

View file

@ -1783,17 +1783,17 @@ createNextDescribe(
await browser.elementByCss('a').click()
browser.waitForElementByCss('#relative-1')
await browser.waitForElementByCss('#relative-1')
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative 1/)
await browser.elementByCss('a').click()
browser.waitForElementByCss('#relative-2')
await browser.waitForElementByCss('#relative-2')
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative 2/)
await browser.elementByCss('button').click()
browser.waitForElementByCss('#relative')
await browser.waitForElementByCss('#relative')
page = await browser.elementByCss('body').text()
expect(page).toMatch(/On relative index/)

View file

@ -23,7 +23,7 @@ createNextDescribe(
pagePath,
pageContent.replaceAll('version-1', 'version-2')
)
browser.waitForElementByCss('#version-2')
await browser.waitForElementByCss('#version-2')
expect(await browser.elementByCss('p').text()).toBe('version-2')
// Verify no hydration mismatch:
@ -33,13 +33,13 @@ createNextDescribe(
pagePath,
pageContent.replaceAll('version-1', 'version-3')
)
browser.waitForElementByCss('#version-3')
await browser.waitForElementByCss('#version-3')
expect(await browser.elementByCss('p').text()).toBe('version-3')
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()
browser.refresh()
await browser.refresh()
// Verify no hydration mismatch:
expect(await hasRedbox(browser)).toBeFalse()

View file

@ -1,7 +1,7 @@
/* eslint-disable jest/no-standalone-expect */
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import type { Response } from 'playwright-chromium'
import type { Response } from 'playwright'
createNextDescribe(
'app-dir action progressive enhancement',
@ -14,7 +14,7 @@ createNextDescribe(
'server-only': 'latest',
},
},
({ next, isNextDev, isNextStart, isNextDeploy }) => {
({ next }) => {
it('should support formData and redirect without JS', async () => {
let responseCode
const browser = await next.browser('/server', {

View file

@ -1,7 +1,7 @@
/* eslint-disable jest/no-standalone-expect */
import { createNextDescribe } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
import { Request, Response, Route } from 'playwright-chromium'
import type { Request, Response, Route } from 'playwright'
import fs from 'fs-extra'
import { join } from 'path'

View file

@ -1,7 +1,7 @@
import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import { BrowserInterface } from 'test/lib/browsers/base'
import { Request } from 'playwright-chromium'
import type { Request } from 'playwright'
const getPathname = (url: string) => {
const urlObj = new URL(url)

View file

@ -1,4 +1,4 @@
import type { Request } from 'playwright-core'
import type { Request } from 'playwright'
import { createNextDescribe } from 'e2e-utils'
import type { BrowserInterface } from '../../../lib/browsers/base'

View file

@ -1,7 +1,7 @@
import { nextTestSetup, FileRef } from 'e2e-utils'
import { check } from 'next-test-utils'
import { join } from 'path'
import { Response } from 'playwright-chromium'
import { Response } from 'playwright'
describe('interception-route-prefetch-cache', () => {
function runTests({ next }: ReturnType<typeof nextTestSetup>) {

View file

@ -1,6 +1,6 @@
import { createNextDescribe } from 'e2e-utils'
import { retry, waitFor } from 'next-test-utils'
import type { Request } from 'playwright-chromium'
import type { Request } from 'playwright'
createNextDescribe(
'app dir - navigation',
@ -35,7 +35,7 @@ createNextDescribe(
}> = []
const browser = await next.browser('/search-params?name=名')
browser.on('request', async (req: Request) => {
async function requestHandler(req: Request) {
const res = await req.response()
if (!res) return
@ -44,7 +44,8 @@ createNextDescribe(
ok: res.ok(),
headers: res.headers(),
})
})
}
browser.on('request', requestHandler)
expect(await browser.elementById('name').text()).toBe('名')
await browser.elementById('link').click()
@ -57,6 +58,9 @@ createNextDescribe(
}),
})
)
browser.off('request', requestHandler)
await browser.close()
})
it('should not reset shallow url updates on prefetch', async () => {

View file

@ -133,7 +133,7 @@ createNextDescribe(
})
`)
await waitForScrollToComplete(browser, { x: 0, y: 10000 })
browser.quit()
browser.close()
})
// Test hot reloading only in development

View file

@ -30,7 +30,6 @@ describe('i18n: Event with stale state - static route previously was dynamic', (
test('Ignore event without query param', async () => {
const browser = await webdriver(next.url, '/sv/static')
browser.close()
const state: HistoryState = {
url: '/[dynamic]?',

View file

@ -30,7 +30,6 @@ describe('Event with stale state - static route previously was dynamic', () => {
test('Ignore event without query param', async () => {
const browser = await webdriver(next.url, '/static')
browser.close()
const state: HistoryState = {
url: '/[dynamic]?',

View file

@ -720,14 +720,14 @@ describe('Middleware Runtime', () => {
requests.push(x.url())
})
browser.elementById('deep-link').click()
browser.waitForElementByCss('[data-query-hello="goodbye"]')
await browser.elementById('deep-link').click()
await browser.waitForElementByCss('[data-query-hello="goodbye"]')
const deepLinkMessage = await getMessageContents()
expect(deepLinkMessage).not.toEqual(ssrMessage)
// Changing the route with a shallow link should not cause a server request
browser.elementById('shallow-link').click()
browser.waitForElementByCss('[data-query-hello="world"]')
await browser.elementById('shallow-link').click()
await browser.waitForElementByCss('[data-query-hello="world"]')
expect(await getMessageContents()).toEqual(deepLinkMessage)
// Check that no server requests were made to ?hello=world,

View file

@ -816,9 +816,9 @@ describe('Middleware Rewrite', () => {
const element = await browser.elementByCss('.title')
expect(await element.text()).toEqual('Parts page')
const logs = await browser.log()
expect(
logs.every((log) => log.source === 'log' || log.source === 'info')
).toEqual(true)
expect(logs).toSatisfyAll(
(log) => log.source === 'log' || log.source === 'info'
)
})
it('should not have unexpected errors', async () => {

View file

@ -470,14 +470,14 @@ describe('Middleware Runtime trailing slash', () => {
requests.push(x.url())
})
browser.elementById('deep-link').click()
browser.waitForElementByCss('[data-query-hello="goodbye"]')
await browser.elementById('deep-link').click()
await browser.waitForElementByCss('[data-query-hello="goodbye"]')
const deepLinkMessage = await getMessageContents()
expect(deepLinkMessage).not.toEqual(ssrMessage)
// Changing the route with a shallow link should not cause a server request
browser.elementById('shallow-link').click()
browser.waitForElementByCss('[data-query-hello="world"]')
await browser.elementById('shallow-link').click()
await browser.waitForElementByCss('[data-query-hello="world"]')
expect(await getMessageContents()).toEqual(deepLinkMessage)
// Check that no server requests were made to ?hello=world,

View file

@ -35,7 +35,7 @@ describe('New Link Behavior with material-ui', () => {
it('should render MuiLink with <a>', async () => {
const browser = await webdriver(next.url, `/`)
const element = await browser.elementByCss('a[href="/about"]')
const element = browser.elementByCss('a[href="/about"]')
const color = await element.getComputedCss('color')
expect(color).toBe('rgb(25, 133, 123)')

View file

@ -160,8 +160,7 @@ describe('Font Optimization', () => {
)
expect(baseFont).toBeDefined()
await browser.waitForElementByCss('#with-font')
await browser.click('#with-font')
await browser.waitForElementByCss('#with-font').click()
await browser.waitForElementByCss('#with-font-container')
const pageFontCss = await browser.elementsByCss(

View file

@ -61,10 +61,8 @@ function getRatio(width, height) {
function runTests(mode) {
it('should load the images', async () => {
let browser
let browser = await webdriver(appPort, '/docs')
try {
browser = await webdriver(appPort, '/docs')
await check(async () => {
const result = await browser.eval(
`document.getElementById('basic-image').naturalWidth`
@ -84,17 +82,13 @@ function runTests(mode) {
)
).toBe(true)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should update the image on src change', async () => {
let browser
let browser = await webdriver(appPort, '/docs/update')
try {
browser = await webdriver(appPort, '/docs/update')
await check(
() => browser.eval(`document.getElementById("update-image").src`),
/test\.jpg/
@ -107,16 +101,13 @@ function runTests(mode) {
/test\.png/
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work when using flexbox', async () => {
let browser
let browser = await webdriver(appPort, '/docs/flex')
try {
browser = await webdriver(appPort, '/docs/flex')
await check(async () => {
const result = await browser.eval(
`document.getElementById('basic-image').width`
@ -128,16 +119,13 @@ function runTests(mode) {
return 'result-correct'
}, /result-correct/)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with layout-fixed so resizing window does not resize image', async () => {
let browser
let browser = await webdriver(appPort, '/docs/layout-fixed')
try {
browser = await webdriver(appPort, '/docs/layout-fixed')
const width = 1200
const height = 700
const delta = 250
@ -162,16 +150,13 @@ function runTests(mode) {
expect(await getComputed(browser, id, 'width')).toBe(width)
expect(await getComputed(browser, id, 'height')).toBe(height)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with layout-intrinsic so resizing window maintains image aspect ratio', async () => {
let browser
let browser = await webdriver(appPort, '/docs/layout-intrinsic')
try {
browser = await webdriver(appPort, '/docs/layout-intrinsic')
const width = 1200
const height = 700
const delta = 250
@ -206,16 +191,13 @@ function runTests(mode) {
1
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with layout-responsive so resizing window maintains image aspect ratio', async () => {
let browser
let browser = await webdriver(appPort, '/docs/layout-responsive')
try {
browser = await webdriver(appPort, '/docs/layout-responsive')
const width = 1200
const height = 700
const delta = 250
@ -250,16 +232,13 @@ function runTests(mode) {
1
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with layout-fill to fill the parent but NOT stretch with viewport', async () => {
let browser
let browser = await webdriver(appPort, '/docs/layout-fill')
try {
browser = await webdriver(appPort, '/docs/layout-fill')
const width = 600
const height = 350
const delta = 150
@ -294,16 +273,13 @@ function runTests(mode) {
1
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with layout-fill to fill the parent and stretch with viewport', async () => {
let browser
let browser = await webdriver(appPort, '/docs/layout-fill')
try {
browser = await webdriver(appPort, '/docs/layout-fill')
const id = 'fill2'
const width = await getComputed(browser, id, 'width')
const height = await getComputed(browser, id, 'height')
@ -348,16 +324,13 @@ function runTests(mode) {
expect(objectFit).toBe('cover')
expect(objectPosition).toBe('left center')
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
it('should work with sizes and automatically use layout-responsive', async () => {
let browser
let browser = await webdriver(appPort, '/docs/sizes')
try {
browser = await webdriver(appPort, '/docs/sizes')
const width = 1200
const height = 700
const delta = 250
@ -394,9 +367,7 @@ function runTests(mode) {
1
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
@ -434,10 +405,8 @@ function runTests(mode) {
}
it('should correctly ignore prose styles', async () => {
let browser
let browser = await webdriver(appPort, '/docs/prose')
try {
browser = await webdriver(appPort, '/docs/prose')
const id = 'prose-image'
// Wait for image to load:
@ -459,19 +428,15 @@ function runTests(mode) {
const computedHeight = await getComputed(browser, id, 'height')
expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(1, 1)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
// Tests that use the `unsized` attribute:
if (mode !== 'dev') {
it('should correctly rotate image', async () => {
let browser
let browser = await webdriver(appPort, '/docs/rotated')
try {
browser = await webdriver(appPort, '/docs/rotated')
const id = 'exif-rotation-image'
// Wait for image to load:
@ -493,9 +458,7 @@ function runTests(mode) {
const computedHeight = await getComputed(browser, id, 'height')
expect(getRatio(computedWidth, computedHeight)).toBeCloseTo(0.5625, 1)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})
}

View file

@ -1,25 +1,11 @@
import React from 'react'
import Image from 'next/legacy/image'
import img from '../public/test.jpg'
const Page = () => {
return (
<div>
<h1>Priority Missing Warning Page</h1>
<Image
id="responsive"
layout="responsive"
src="/wide.png"
width="1200"
height="700"
/>
<Image
id="fixed"
layout="fixed"
src="/test.jpg"
width="400"
height="400"
/>
<footer>Priority Missing Warning Footer</footer>
<Image id="responsive" src={img} />
</div>
)
}

View file

@ -915,9 +915,8 @@ function runTests(mode) {
})
it('should warn when priority prop is missing on LCP image', async () => {
let browser
let browser = await webdriver(appPort, '/priority-missing-warning')
try {
browser = await webdriver(appPort, '/priority-missing-warning')
// Wait for image to load:
await check(async () => {
const result = await browser.eval(
@ -934,12 +933,10 @@ function runTests(mode) {
.join('\n')
expect(await hasRedbox(browser)).toBe(false)
expect(warnings).toMatch(
/Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm
/Image with src (.*)test(.*) was detected as the Largest Contentful Paint/gm
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})

View file

@ -1,13 +1,11 @@
import React from 'react'
import Image from 'next/image'
import img from '../../public/test.jpg'
const Page = () => {
return (
<div>
<h1>Priority Missing Warning Page</h1>
<Image id="responsive" src="/wide.png" width="1200" height="700" />
<Image id="fixed" src="/test.jpg" width="400" height="400" />
<footer>Priority Missing Warning Footer</footer>
<Image id="responsive" src={img} />
</div>
)
}

View file

@ -990,9 +990,8 @@ function runTests(mode) {
})
it('should warn when priority prop is missing on LCP image', async () => {
let browser
let browser = await webdriver(appPort, '/priority-missing-warning')
try {
browser = await webdriver(appPort, '/priority-missing-warning')
// Wait for image to load:
await check(async () => {
const result = await browser.eval(
@ -1009,12 +1008,10 @@ function runTests(mode) {
.join('\n')
expect(await hasRedbox(browser)).toBe(false)
expect(warnings).toMatch(
/Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm
/Image with src (.*)test(.*) was detected as the Largest Contentful Paint/gm
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})

View file

@ -1,13 +1,11 @@
import React from 'react'
import Image from 'next/image'
import img from '../public/test.jpg'
const Page = () => {
return (
<div>
<h1>Priority Missing Warning Page</h1>
<Image id="responsive" src="/wide.png" width="1200" height="700" />
<Image id="fixed" src="/test.jpg" width="400" height="400" />
<footer>Priority Missing Warning Footer</footer>
<Image id="responsive" src={img} />
</div>
)
}

View file

@ -991,9 +991,8 @@ function runTests(mode) {
})
it('should warn when priority prop is missing on LCP image', async () => {
let browser
let browser = await webdriver(appPort, '/priority-missing-warning')
try {
browser = await webdriver(appPort, '/priority-missing-warning')
// Wait for image to load:
await check(async () => {
const result = await browser.eval(
@ -1005,17 +1004,15 @@ 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)
expect(warnings).toMatch(
/Image with src (.*)wide.png(.*) was detected as the Largest Contentful Paint/gm
/Image with src (.*)test(.*) was detected as the Largest Contentful Paint/gm
)
} finally {
if (browser) {
await browser.close()
}
await browser.close()
}
})

View file

@ -154,8 +154,7 @@ const runTests = (isDev) => {
)
expect(documentBIScripts.length).toBe(2)
await browser.waitForElementByCss('[href="/page1"]')
await browser.click('[href="/page1"]')
await browser.waitForElementByCss('[href="/page1"]').click()
await browser.waitForElementByCss('.container')
@ -179,10 +178,8 @@ const runTests = (isDev) => {
expect(text).toBe('aaabbbccc')
// Navigate to different page and back
await browser.waitForElementByCss('[href="/page9"]')
await browser.click('[href="/page9"]')
await browser.waitForElementByCss('[href="/page4"]')
await browser.click('[href="/page4"]')
await browser.waitForElementByCss('[href="/page9"]').click()
await browser.waitForElementByCss('[href="/page4"]').click()
await browser.waitForElementByCss('#onload-div-1')
const sameText = await browser.elementById('onload-div-1').text()
@ -233,12 +230,9 @@ const runTests = (isDev) => {
browser = await webdriver(appPort, '/')
// Navigate away and back to page
await browser.waitForElementByCss('[href="/page5"]')
await browser.click('[href="/page5"]')
await browser.waitForElementByCss('[href="/"]')
await browser.click('[href="/"]')
await browser.waitForElementByCss('[href="/page5"]')
await browser.click('[href="/page5"]')
await browser.waitForElementByCss('[href="/page5"]').click()
await browser.waitForElementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/page5"]').click()
await browser.waitForElementByCss('.container')
await waitFor(1000)
@ -263,7 +257,7 @@ const runTests = (isDev) => {
)
const output = stdout + stderr
expect(output.replace(/\n|\r/g, '')).toMatch(
expect(output.replace(/[\n\r]/g, '')).toMatch(
/It looks like you're trying to use Partytown with next\/script but do not have the required package\(s\) installed.Please install Partytown by running:.*?(npm|pnpm|yarn) (install|add) (--save-dev|--dev) @builder.io\/partytownIf you are not trying to use Partytown, please disable the experimental "nextScriptWorkers" flag in next.config.js./
)
})
@ -279,10 +273,8 @@ const runTests = (isDev) => {
expect(text).toBe('aaa')
// Navigate to different page and back
await browser.waitForElementByCss('[href="/page9"]')
await browser.click('[href="/page9"]')
await browser.waitForElementByCss('[href="/page8"]')
await browser.click('[href="/page8"]')
await browser.waitForElementByCss('[href="/page9"]').click()
await browser.waitForElementByCss('[href="/page8"]').click()
await browser.waitForElementByCss('.container')
const sameText = await browser.elementById('text').text()

View file

@ -2,13 +2,8 @@ export type Event = 'request'
/**
* This is the base Browser interface all browser
* classes should build off of, it is the bare
* classes should build on, it is the bare
* methods we aim to support across tests
*
* They will always await last executed command.
* The interface is mutable - it doesn't have to be in sequence.
*
* You can manually await this interface to wait for completion of the last scheduled command.
*/
export abstract class BrowserInterface implements PromiseLike<any> {
private promise?: Promise<any>
@ -22,108 +17,76 @@ export abstract class BrowserInterface implements PromiseLike<any> {
protected chain<T>(
nextCall: (current: any) => T | PromiseLike<T>
): BrowserInterface & Promise<T> {
if (!this.promise) {
this.promise = Promise.resolve(this)
}
this.promise = this.promise.then(nextCall)
this.then = (...args) => this.promise.then(...args)
this.catch = (...args) => this.promise.catch(...args)
this.finally = (...args) => this.promise.finally(...args)
return this
}
const promise = Promise.resolve(this.promise).then(nextCall)
/**
* This function will run in chain - it will wait for previous commands.
* But it won't have an effect on chain value and chain will still be green if this throws.
*/
protected chainWithReturnValue<T>(
callback: (...args: any[]) => Promise<T>
): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.chain(async (...args: any[]) => {
try {
resolve(await callback(...args))
} catch (error) {
reject(error)
}
})
function get(target: BrowserInterface, p: string | symbol): any {
switch (p) {
case 'promise':
return promise
case 'then':
return promise.then.bind(promise)
case 'catch':
return promise.catch.bind(promise)
case 'finally':
return promise.finally.bind(promise)
default:
return target[p]
}
}
return new Proxy<any>(this, {
get,
})
}
async setup(
protected chainWithReturnValue<T>(
callback: (value: any) => T | PromiseLike<T>
): Promise<T> {
return Promise.resolve(this.promise).then(callback)
}
abstract setup(
browserName: string,
locale: string,
javaScriptEnabled: boolean,
ignoreHttpsErrors: boolean,
headless: boolean
): Promise<void> {}
async close(): Promise<void> {}
async quit(): Promise<void> {}
): Promise<void>
abstract close(): Promise<void>
elementsByCss(selector: string): BrowserInterface[] {
return [this]
}
elementByCss(selector: string): BrowserInterface {
return this
}
elementById(selector: string): BrowserInterface {
return this
}
touchStart(): BrowserInterface {
return this
}
click(opts?: { modifierKey?: boolean }): BrowserInterface {
return this
}
keydown(key: string): BrowserInterface {
return this
}
keyup(key: string): BrowserInterface {
return this
}
focusPage(): BrowserInterface {
return this
}
type(text: string): BrowserInterface {
return this
}
moveTo(): BrowserInterface {
return this
}
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
waitForElementByCss(selector: string, timeout?: number): BrowserInterface {
return this
}
waitForCondition(snippet: string, timeout?: number): BrowserInterface {
return this
}
abstract waitForElementByCss(
selector: string,
timeout?: number
): BrowserInterface
abstract waitForCondition(snippet: string, timeout?: number): BrowserInterface
/**
* Use browsers `go back` functionality.
*/
back(options?: any): BrowserInterface {
return this
}
abstract back(options?: any): BrowserInterface
/**
* Use browsers `go forward` functionality. Inverse of back.
*/
forward(options?: any): BrowserInterface {
return this
}
refresh(): BrowserInterface {
return this
}
setDimensions(opts: { height: number; width: number }): BrowserInterface {
return this
}
addCookie(opts: { name: string; value: string }): BrowserInterface {
return this
}
deleteCookies(): BrowserInterface {
return this
}
on(event: Event, cb: (...args: any[]) => void) {}
off(event: Event, cb: (...args: any[]) => void) {}
async loadPage(
abstract forward(options?: any): BrowserInterface
abstract refresh(): BrowserInterface
abstract setDimensions(opts: {
height: number
width: number
}): BrowserInterface
abstract addCookie(opts: { name: string; value: string }): BrowserInterface
abstract deleteCookies(): BrowserInterface
abstract on(event: Event, cb: (...args: any[]) => void): void
abstract off(event: Event, cb: (...args: any[]) => void): void
abstract loadPage(
url: string,
{
disableCache,
@ -136,44 +99,23 @@ export abstract class BrowserInterface implements PromiseLike<any> {
beforePageLoad?: Function
pushErrorAsConsoleLog?: boolean
}
): Promise<void> {}
async get(url: string): Promise<void> {}
): Promise<void>
abstract get(url: string): Promise<void>
async getValue<T = any>(): Promise<T> {
return
}
async getAttribute<T = any>(name: string): Promise<T> {
return
}
async eval<T = any>(snippet: string | Function, ...args: any[]): Promise<T> {
return
}
async evalAsync<T = any>(
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> {
return
}
async text(): Promise<string> {
return ''
}
async getComputedCss(prop: string): Promise<string> {
return ''
}
async hasElementByCssSelector(selector: string): Promise<boolean> {
return false
}
async log(): Promise<
): Promise<T>
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 }[]
> {
return []
}
async websocketFrames(): Promise<any[]> {
return []
}
async url(): Promise<string> {
return ''
}
async waitForIdleNetwork(): Promise<void> {}
>
abstract websocketFrames(): Promise<any[]>
abstract url(): Promise<string>
abstract waitForIdleNetwork(): Promise<void>
}

View file

@ -9,7 +9,7 @@ import {
Page,
ElementHandle,
devices,
} from 'playwright-chromium'
} from 'playwright'
import path from 'path'
let page: Page
@ -50,11 +50,7 @@ export class Playwright extends BrowserInterface {
private eventCallbacks: Record<Event, Set<(...args: any[]) => void>> = {
request: new Set(),
}
private async initContextTracing(
url: string,
page: Page,
context: BrowserContext
) {
private async initContextTracing(url: string, context: BrowserContext) {
if (!tracePlaywright) {
return
}
@ -69,17 +65,13 @@ export class Playwright extends BrowserInterface {
sources: true,
})
this.activeTrace = encodeURIComponent(url)
page.on('close', async () => {
await teardown(this.teardownTracing.bind(this))
})
} catch (e) {
this.activeTrace = undefined
}
}
private async teardownTracing() {
if (!tracePlaywright || !this.activeTrace) {
if (!this.activeTrace) {
return
}
@ -163,6 +155,11 @@ export class Playwright extends BrowserInterface {
contextHasJSEnabled = javaScriptEnabled
}
async close(): Promise<void> {
await teardown(this.teardownTracing.bind(this))
await page?.close()
}
async launchBrowser(browserName: string, launchOptions: Record<string, any>) {
if (browserName === 'safari') {
return await webkit.launch(launchOptions)
@ -201,13 +198,15 @@ export class Playwright extends BrowserInterface {
beforePageLoad?: (...args: any[]) => void
}
) {
await this.close()
// clean-up existing pages
for (const oldPage of context.pages()) {
await oldPage.close()
}
await this.initContextTracing(url, context)
page = await context.newPage()
await this.initContextTracing(url, page, context)
// in development compilation can take longer due to
// lower CPU availability in GH actions
@ -221,7 +220,7 @@ export class Playwright extends BrowserInterface {
console.log('browser log:', msg)
pageLogs.push({ source: msg.type(), message: msg.text() })
})
page.on('crash', (page) => {
page.on('crash', () => {
console.error('page crashed')
})
page.on('pageerror', (error) => {

View file

@ -1,362 +0,0 @@
import path from 'path'
import resolveFrom from 'resolve-from'
import { execSync } from 'child_process'
import { Options as ChromeOptions } from 'selenium-webdriver/chrome'
import { Options as SafariOptions } from 'selenium-webdriver/safari'
import { Options as FireFoxOptions } from 'selenium-webdriver/firefox'
import { Builder, By, ThenableWebDriver, until } from 'selenium-webdriver'
import { BrowserInterface } from './base'
const {
BROWSERSTACK,
BROWSERSTACK_USERNAME,
BROWSERSTACK_ACCESS_KEY,
CHROME_BIN,
LEGACY_SAFARI,
SKIP_LOCAL_SELENIUM_SERVER,
} = process.env
if (process.env.ChromeWebDriver) {
process.env.PATH = `${process.env.ChromeWebDriver}${path.delimiter}${process.env.PATH}`
}
let seleniumServer: any
let browserStackLocal: any
let browser: ThenableWebDriver
export async function quit() {
await Promise.all([
browser?.quit(),
new Promise<void>((resolve) => {
browserStackLocal
? browserStackLocal.killAllProcesses(() => resolve())
: resolve()
}),
])
seleniumServer?.kill()
browser = undefined
browserStackLocal = undefined
seleniumServer = undefined
}
export class Selenium extends BrowserInterface {
private browserName: string
// TODO: support setting locale
async setup(
browserName: string,
locale: string,
javaScriptEnabled: boolean,
headless: boolean
) {
if (browser) return
this.browserName = browserName
let capabilities = {}
const isSafari = browserName === 'safari'
const isFirefox = browserName === 'firefox'
const isIE = browserName === 'internet explorer'
const isBrowserStack = BROWSERSTACK
const localSeleniumServer = SKIP_LOCAL_SELENIUM_SERVER !== 'true'
// install conditional packages globally so the entire
// monorepo doesn't need to rebuild when testing
let globalNodeModules: string
if (isBrowserStack || localSeleniumServer) {
globalNodeModules = execSync('npm root -g').toString().trim()
}
if (isBrowserStack) {
const { Local } = require(resolveFrom(
globalNodeModules,
'browserstack-local'
))
browserStackLocal = new Local()
const localBrowserStackOpts = {
key: process.env.BROWSERSTACK_ACCESS_KEY,
// Add a unique local identifier to run parallel tests
// on BrowserStack
localIdentifier: new Date().getTime(),
}
await new Promise<void>((resolve, reject) => {
browserStackLocal.start(localBrowserStackOpts, (err) => {
if (err) return reject(err)
console.log(
'Started BrowserStackLocal',
browserStackLocal.isRunning()
)
resolve()
})
})
const safariOpts = {
os: 'OS X',
os_version: 'Mojave',
browser: 'Safari',
}
const safariLegacyOpts = {
os: 'OS X',
os_version: 'Sierra',
browserName: 'Safari',
browser_version: '10.1',
}
const ieOpts = {
os: 'Windows',
os_version: '10',
browser: 'IE',
}
const firefoxOpts = {
os: 'Windows',
os_version: '10',
browser: 'Firefox',
}
const sharedOpts = {
'browserstack.local': true,
'browserstack.video': false,
'browserstack.user': BROWSERSTACK_USERNAME,
'browserstack.key': BROWSERSTACK_ACCESS_KEY,
'browserstack.localIdentifier': localBrowserStackOpts.localIdentifier,
}
capabilities = {
...capabilities,
...sharedOpts,
...(isIE ? ieOpts : {}),
...(isSafari ? (LEGACY_SAFARI ? safariLegacyOpts : safariOpts) : {}),
...(isFirefox ? firefoxOpts : {}),
}
} else if (localSeleniumServer) {
console.log('Installing selenium server')
const seleniumServerMod = require(resolveFrom(
globalNodeModules,
'selenium-standalone'
))
await new Promise<void>((resolve, reject) => {
seleniumServerMod.install((err) => {
if (err) return reject(err)
resolve()
})
})
console.log('Starting selenium server')
await new Promise<void>((resolve, reject) => {
seleniumServerMod.start((err, child) => {
if (err) return reject(err)
seleniumServer = child
resolve()
})
})
console.log('Started selenium server')
}
let chromeOptions = new ChromeOptions()
let firefoxOptions = new FireFoxOptions()
let safariOptions = new SafariOptions()
if (headless) {
const screenSize = { width: 1280, height: 720 }
chromeOptions = chromeOptions.headless().windowSize(screenSize) as any
firefoxOptions = firefoxOptions.headless().windowSize(screenSize)
}
if (CHROME_BIN) {
chromeOptions = chromeOptions.setChromeBinaryPath(
path.resolve(CHROME_BIN)
)
}
let seleniumServerUrl
if (isBrowserStack) {
seleniumServerUrl = 'http://hub-cloud.browserstack.com/wd/hub'
} else if (localSeleniumServer) {
seleniumServerUrl = `http://localhost:4444/wd/hub`
}
browser = new Builder()
.usingServer(seleniumServerUrl)
.withCapabilities(capabilities)
.forBrowser(browserName)
.setChromeOptions(chromeOptions)
.setFirefoxOptions(firefoxOptions)
.setSafariOptions(safariOptions)
.build()
}
async get(url: string): Promise<void> {
return browser.get(url)
}
async loadPage(url: string) {
// in chrome we use a new tab for testing
if (this.browserName === 'chrome') {
const initialHandle = await browser.getWindowHandle()
await browser.switchTo().newWindow('tab')
const newHandle = await browser.getWindowHandle()
await browser.switchTo().window(initialHandle)
await browser.close()
await browser.switchTo().window(newHandle)
// clean-up extra windows created from links and such
for (const handle of await browser.getAllWindowHandles()) {
if (handle !== newHandle) {
await browser.switchTo().window(handle)
await browser.close()
}
}
await browser.switchTo().window(newHandle)
} else {
await browser.get('about:blank')
}
return browser.get(url)
}
back(): BrowserInterface {
return this.chain(() => {
return browser.navigate().back()
})
}
forward(): BrowserInterface {
return this.chain(() => {
return browser.navigate().forward()
})
}
refresh(): BrowserInterface {
return this.chain(() => {
return browser.navigate().refresh()
})
}
setDimensions({
width,
height,
}: {
height: number
width: number
}): BrowserInterface {
return this.chain(() =>
browser.manage().window().setRect({ width, height, x: 0, y: 0 })
)
}
addCookie(opts: { name: string; value: string }): BrowserInterface {
return this.chain(() => browser.manage().addCookie(opts))
}
deleteCookies(): BrowserInterface {
return this.chain(() => browser.manage().deleteAllCookies())
}
elementByCss(selector: string) {
return this.chain(() => {
return browser.findElement(By.css(selector)).then((el: any) => {
el.selector = selector
el.text = () => el.getText()
el.getComputedCss = (prop) => el.getCssValue(prop)
el.type = (text) => el.sendKeys(text)
el.getValue = () =>
browser.executeScript(
`return document.querySelector('${selector}').value`
)
return el
})
})
}
elementById(sel) {
return this.elementByCss(`#${sel}`)
}
getValue() {
return this.chain((el) =>
browser.executeScript(
`return document.querySelector('${el.selector}').value`
)
) as any
}
text() {
return this.chain((el) => el.getText()) as any
}
type(text) {
return this.chain((el) => el.sendKeys(text))
}
moveTo() {
return this.chain((el) => {
return browser
.actions()
.move({ origin: el })
.perform()
.then(() => el)
})
}
async getComputedCss(prop: string) {
return this.chain((el) => {
return el.getCssValue(prop)
}) as any
}
async getAttribute<T = any>(attr) {
return this.chain((el) => el.getAttribute(attr)) as T
}
async hasElementByCssSelector(selector: string) {
return this.eval(`!!document.querySelector('${selector}')`) as any
}
click() {
return this.chain((el) => {
return el.click().then(() => el)
})
}
elementsByCss(sel) {
return this.chain(() =>
browser.findElements(By.css(sel))
) as any as BrowserInterface[]
}
waitForElementByCss(sel, timeout) {
return this.chain(() =>
browser.wait(until.elementLocated(By.css(sel)), timeout)
)
}
waitForCondition(condition, timeout) {
return this.chain(() =>
browser.wait(async (driver) => {
return driver.executeScript('return ' + condition).catch(() => false)
}, timeout)
)
}
async eval<T = any>(snippet) {
if (typeof snippet === 'string' && !snippet.startsWith('return')) {
snippet = `return ${snippet}`
}
return browser.executeScript<T>(snippet)
}
async evalAsync<T = any>(snippet) {
if (typeof snippet === 'string' && !snippet.startsWith('return')) {
snippet = `return ${snippet}`
}
return browser.executeAsyncScript<T>(snippet)
}
async log() {
return this.chain(() => browser.manage().logs().get('browser')) as any
}
async url() {
return this.chain(() => browser.getCurrentUrl()) as any
}
}

View file

@ -284,6 +284,9 @@ export function nextTestSetup(
}
}
/**
* @deprecated use `nextTestSetup` directly.
*/
export function createNextDescribe(
name: string,
options: Parameters<typeof createNext>[0] & {

View file

@ -26,22 +26,21 @@ if (isBrowserStack) {
}
}
let browserQuit: () => Promise<void>
let browserTeardown: (() => Promise<void>)[] = []
let browserQuit: (() => Promise<void>) | undefined
if (typeof afterAll === 'function') {
afterAll(async () => {
await Promise.all(browserTeardown.map((f) => f())).catch((e) =>
console.error('browser teardown', e)
)
if (browserQuit) {
await browserQuit()
}
})
}
export const USE_SELENIUM = Boolean(
process.env.LEGACY_SAFARI ||
process.env.BROWSER_NAME === 'internet explorer' ||
process.env.SKIP_LOCAL_SELENIUM_SERVER
)
/**
*
* @param appPortOrUrl can either be the port or the full URL
@ -94,11 +93,7 @@ export default async function webdriver(
} = options
// we import only the needed interface
if (USE_SELENIUM) {
const { Selenium, quit } = await import('./browsers/selenium')
CurrentInterface = Selenium
browserQuit = quit
} else if (
if (
process.env.RECORD_REPLAY === 'true' ||
process.env.RECORD_REPLAY === '1'
) {
@ -139,6 +134,8 @@ export default async function webdriver(
})
console.log(`\n> Loaded browser with ${fullUrl}\n`)
browserTeardown.push(browser.close.bind(browser))
// Wait for application to hydrate
if (waitHydration) {
console.log(`\n> Waiting hydration for ${fullUrl}\n`)
@ -198,3 +195,5 @@ export default async function webdriver(
}
return browser
}
export { BrowserInterface }

View file

@ -715,8 +715,9 @@ createNextDescribe(
// @ts-expect-error Exists on window
window.__DATA_BE_GONE = 'true'
})
await browser.waitForElementByCss('#to-nonexistent-page')
await browser.click('#to-nonexistent-page')
await browser
.waitForElementByCss('#to-nonexistent-page')
.click('#to-nonexistent-page')
await browser.waitForElementByCss('.about-page')
const oldData = await browser.eval(`window.__DATA_BE_GONE`)