Files
Benjamin WoodruffandGitHub 727a3ffa86 [ci] Skip apt-installed dependencies in playwright install step (#95535)
The hypothesis is that this is just installing some font stuff that
isn't actually needed for Chromium to work:

```
The following additional packages will be installed:
  libnss3-tools xfonts-encodings xfonts-utils
Recommended packages:
  fonts-ipafont-mincho fonts-liberation-sans-narrow fonts-tlwg-loma
The following NEW packages will be installed:
  fonts-freefont-ttf fonts-ipafont-gothic fonts-liberation fonts-tlwg-loma-otf
  fonts-unifont fonts-wqy-zenhei xfonts-cyrillic xfonts-encodings
  xfonts-scalable xfonts-utils
The following packages will be upgraded:
  libnss3 libnss3-tools
2 upgraded, 10 newly installed, 0 to remove and 35 not upgraded.
```

`apt` is very slow, so skipping this step should significantly reduce
setup time per test shard.

It seems like this isn't always true for other browsers, so only skip
this for Chromium (that's fine, that's what most of our jobs use
anyways).

On 16 core arm runners, "Install Playwright Browsers" typically took
18-24 seconds. Now it takes 5-6s.

Let's estimate that saves about 15s per Linux job that installs
Chromium. Claude estimates that there are 73 chromium-only Linux jobs on
this PR:

<img width="891" height="148" alt="Screenshot 2026-07-06 at 10 30 16 PM"
src="https://github.com/user-attachments/assets/725893fa-9f6f-4806-a3b4-9858ab0ebe66"
/>

So that's 15*73/60=18.25 minutes saved per PR push (the average PR gets
many pushes). At the public (before any volume discounts) price of
$0.026/minute for the 16 core arm runners, that comes out to $0.4745 per
PR push saved.

This workflow has had 3,894 runs in the last 30 days, so that could be
$1,847/mo saved, but many of those workflow runs were canceled, and some
likely failed before getting to these jobs, so the actual savings are
probably somewhere around half of that -- scroll through
https://github.com/vercel/next.js/actions/workflows/.github/workflows/build_and_test.yml
and see that a little less than half of the workflow runs end up
canceled.
2026-07-07 09:48:46 +02:00

261 lines
7.9 KiB
TypeScript

/* eslint-env jest */
import path from 'path'
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { expectScrolledTo } from './scroll-position.util'
describe('Client navigation with URL hash', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixture'),
env: {
TEST_STRICT_NEXT_HEAD: String(true),
},
})
describe('when hash changes', () => {
describe('check hydration mis-match', () => {
it('should not have hydration mis-match for hash link', async () => {
const browser = await next.browser('/nav/hash-changes')
const browserLogs = await browser.log()
let found = false
browserLogs.forEach((log) => {
console.log('log.message', log.message)
if (log.message.includes('Warning: Prop')) {
found = true
}
})
expect(found).toEqual(false)
})
})
describe('when hash change via Link', () => {
it('should not run getInitialProps', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser.elementByCss('#via-link').click()
await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('COUNT: 0')
})
await browser.close()
})
it('should scroll to the specified position on the same page', async () => {
const browser = await next.browser('/nav/hash-changes')
// Scrolls to item 400 on the page
await browser.elementByCss('#scroll-to-item-400').click()
await expectScrolledTo(browser, "document.getElementById('item-400')")
// Scrolls back to top when scrolling to `#` with no value.
await browser.elementByCss('#via-empty-hash').click()
await retry(async () => {
expect(await browser.eval('window.pageYOffset')).toBe(0)
})
// Scrolls to item 400 on the page
await browser.elementByCss('#scroll-to-item-400').click()
await expectScrolledTo(browser, "document.getElementById('item-400')")
// Scrolls back to top when clicking link with href `#top`.
await browser.elementByCss('#via-top-hash').click()
await retry(async () => {
expect(await browser.eval('window.pageYOffset')).toBe(0)
})
// Scrolls to cjk anchor on the page
await browser.elementByCss('#scroll-to-cjk-anchor').click()
await expectScrolledTo(browser, "document.getElementById('中文')")
})
it('should not scroll to hash when scroll={false} is set', async () => {
const browser = await next.browser('/nav/hash-changes')
const curScroll = await browser.eval(
'document.documentElement.scrollTop'
)
await browser.elementByCss('#scroll-to-name-item-400-no-scroll').click()
await retry(async () => {
expect(await browser.eval('document.documentElement.scrollTop')).toBe(
curScroll
)
})
})
it('should scroll to the specified position on the same page with a name property', async () => {
const browser = await next.browser('/nav/hash-changes')
// Scrolls to item 400 with name="name-item-400" on the page
await browser.elementByCss('#scroll-to-name-item-400').click()
await expectScrolledTo(
browser,
"document.getElementsByName('name-item-400')[0]"
)
// Scrolls back to top when scrolling to `#` with no value.
await browser.elementByCss('#via-empty-hash').click()
await retry(async () => {
expect(await browser.eval('window.pageYOffset')).toBe(0)
})
})
it('should scroll to the specified position to a new page', async () => {
const browser = await next.browser('/nav')
// Scrolls to item 400 on the page
await browser
.elementByCss('#scroll-to-hash')
.click()
.waitForElementByCss('#hash-changes-page')
await expectScrolledTo(browser, "document.getElementById('item-400')")
})
it('should scroll to the specified CJK position to a new page', async () => {
const browser = await next.browser('/nav')
// Scrolls to CJK anchor on the page
await browser
.elementByCss('#scroll-to-cjk-hash')
.click()
.waitForElementByCss('#hash-changes-page')
await expectScrolledTo(browser, "document.getElementById('中文')")
})
it('Should update asPath', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser.elementByCss('#via-link').click()
await retry(async () => {
expect(await browser.elementByCss('div#asPath').text()).toBe(
'ASPATH: /nav/hash-changes#via-link'
)
})
})
})
describe('when hash change via A tag', () => {
it('should not run getInitialProps', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser.elementByCss('#via-a').click()
await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('COUNT: 0')
})
await browser.close()
})
})
describe('when hash get removed', () => {
it('should not run getInitialProps', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser
.elementByCss('#via-a')
.click()
.elementByCss('#page-url')
.click()
await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('COUNT: 1')
})
await browser.close()
})
it('should not run getInitialProps when removing via back', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser.elementByCss('#scroll-to-item-400').click()
await browser.back()
await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('COUNT: 0')
})
await browser.close()
})
})
describe('when hash set to empty', () => {
it('should not run getInitialProps', async () => {
const browser = await next.browser('/nav/hash-changes')
await browser
.elementByCss('#via-a')
.click()
.elementByCss('#via-empty-hash')
.click()
await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe('COUNT: 0')
})
await browser.close()
})
})
})
describe('when hash changes with state', () => {
describe('when passing state via hash change', () => {
it('should increment the history state counter', async () => {
const browser = await next.browser('/nav/hash-changes-with-state#')
await browser
.elementByCss('#increment-history-count')
.click()
.elementByCss('#increment-history-count')
.click()
await retry(async () => {
expect(await browser.elementByCss('div#history-count').text()).toBe(
'HISTORY COUNT: 2'
)
})
const counter = await browser.elementByCss('p').text()
// getInitialProps should not be called with only hash changes
expect(counter).toBe('COUNT: 0')
await browser.close()
})
it('should increment the shallow history state counter', async () => {
const browser = await next.browser('/nav/hash-changes-with-state#')
await browser
.elementByCss('#increment-shallow-history-count')
.click()
.elementByCss('#increment-shallow-history-count')
.click()
await retry(async () => {
expect(
await browser.elementByCss('div#shallow-history-count').text()
).toBe('SHALLOW HISTORY COUNT: 2')
})
const counter = await browser.elementByCss('p').text()
expect(counter).toBe('COUNT: 0')
await browser.close()
})
})
})
})