Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
304 lines
10 KiB
TypeScript
304 lines
10 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { Playwright } from 'next-webdriver'
|
|
|
|
const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18
|
|
|
|
// These tests are defined here and used in `app-dir.test.ts` and
|
|
// `pages-dir.test.ts` so that both test suites can be run in parallel.
|
|
export function runSharedTests(type: 'app' | 'pages') {
|
|
describe(`next-form - ${type} dir`, () => {
|
|
const { next, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
nextConfig: {
|
|
typescript: {
|
|
ignoreBuildErrors: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
const isAppDir = type === 'app'
|
|
const pathPrefix = isAppDir ? '' : '/pages-dir'
|
|
|
|
it(
|
|
'should soft-navigate on submit' +
|
|
(isAppDir ? ' and show the prefetched loading state' : ''),
|
|
async () => {
|
|
const session = await next.browser(pathPrefix + '/forms/basic')
|
|
const navigationTracker = await trackMpaNavs(session)
|
|
|
|
const searchInput = await session.elementByCss('input[name="query"]')
|
|
await searchInput.fill('my search')
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
if (isAppDir) {
|
|
// we should have prefetched a loading state, so it should be displayed
|
|
await session.waitForElementByCss('#loading')
|
|
}
|
|
|
|
const result = await session
|
|
.waitForElementByCss('#search-results')
|
|
.text()
|
|
expect(result).toMatch(/query: "my search"/)
|
|
|
|
expect(await navigationTracker.didMpaNavigate()).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('should soft-navigate to the formAction url of the submitter', async () => {
|
|
const session = await next.browser(
|
|
pathPrefix + '/forms/button-formaction'
|
|
)
|
|
const navigationTracker = await trackMpaNavs(session)
|
|
|
|
const searchInput = await session.elementByCss('input[name="query"]')
|
|
await searchInput.fill('my search')
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
// we didn't prefetch a loading state, so we don't know if it'll be displayed
|
|
// TODO: is this correct? it'll probably be there in dev, but what about prod?
|
|
// await session.waitForElementByCss('#loading')
|
|
|
|
const result = await session.waitForElementByCss('#search-results').text()
|
|
expect(result).toMatch(/query: "my search"/)
|
|
|
|
expect(await navigationTracker.didMpaNavigate()).toBe(false)
|
|
})
|
|
|
|
// `<form action={someFunction}>` is only supported in React 19.x
|
|
;(isReact18 ? describe.skip : describe)(
|
|
'functions passed to action',
|
|
() => {
|
|
it.each([
|
|
{
|
|
name: 'client action',
|
|
path: '/forms/with-function/action-client',
|
|
},
|
|
...(isAppDir
|
|
? [
|
|
{
|
|
name: 'server action',
|
|
path: '/forms/with-function/action-server',
|
|
},
|
|
{
|
|
name: 'server action (closure)',
|
|
path: '/forms/with-function/action-server-closure',
|
|
},
|
|
]
|
|
: []),
|
|
])('runs $name', async ({ path }) => {
|
|
const session = await next.browser(pathPrefix + path)
|
|
const navigationTracker = await trackMpaNavs(session) // actions should not MPA-navigate either.
|
|
|
|
const searchInput = await session.elementByCss('input[name="query"]')
|
|
await searchInput.fill('will not be a search')
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
const result = await session
|
|
.waitForElementByCss('#redirected-results')
|
|
.text()
|
|
expect(result).toMatch(/query: "will not be a search"/)
|
|
|
|
expect(await navigationTracker.didMpaNavigate()).toBe(false)
|
|
})
|
|
}
|
|
)
|
|
|
|
// `<button formAction={someFunction}>` is only supported in React 19.x
|
|
;(isReact18 ? describe.skip : describe)(
|
|
'functions passed to formAction',
|
|
() => {
|
|
it.each([
|
|
{
|
|
// TODO(lubieowoce): figure out why the client navigation is failing in pages dir
|
|
// (see "pages-dir/forms/with-function/button-formaction-client/index.tsx" for more)
|
|
name: 'client action',
|
|
path: '/forms/with-function/button-formaction-client',
|
|
},
|
|
...(isAppDir
|
|
? [
|
|
{
|
|
name: 'server action',
|
|
path: '/forms/with-function/button-formaction-server',
|
|
},
|
|
{
|
|
name: 'server action (closure)',
|
|
path: '/forms/with-function/button-formaction-server-closure',
|
|
},
|
|
]
|
|
: []),
|
|
])(
|
|
"runs $name from submitter and doesn't warn about unsupported attributes",
|
|
async ({ path }) => {
|
|
const session = await next.browser(pathPrefix + path)
|
|
const navigationTracker = await trackMpaNavs(session) // actions should not MPA-navigate either.
|
|
|
|
const searchInput = await session.elementByCss(
|
|
'input[name="query"]'
|
|
)
|
|
await searchInput.fill('will not be a search')
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
const result = await session
|
|
.waitForElementByCss('#redirected-results')
|
|
.text()
|
|
expect(result).toMatch(/query: "will not be a search"/)
|
|
|
|
expect(await navigationTracker.didMpaNavigate()).toBe(false)
|
|
|
|
if (isNextDev) {
|
|
const logs = (await session.log()).map((item) => item.message)
|
|
|
|
expect(logs).not.toContainEqual(
|
|
expect.stringMatching(
|
|
/<Form>'s `.+?` was set to an unsupported value/
|
|
)
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
|
|
describe('unsupported attributes on submitter', () => {
|
|
it.each([
|
|
{ name: 'formEncType', baseName: 'encType' },
|
|
{ name: 'formMethod', baseName: 'method' },
|
|
{ name: 'formTarget', baseName: 'target' },
|
|
])(
|
|
'should warn if submitter sets "$name" to an unsupported value and fall back to default submit behavior',
|
|
async ({ name: attributeName, baseName: attributeBaseName }) => {
|
|
const session = await next.browser(
|
|
pathPrefix +
|
|
`/forms/button-formaction-unsupported?attribute=${attributeName}`
|
|
)
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
const logs = await session.log()
|
|
|
|
if (isNextDev) {
|
|
expect(logs).toContainEqual(
|
|
expect.objectContaining({
|
|
source: 'error',
|
|
message: expect.stringContaining(
|
|
`<Form>'s \`${attributeBaseName}\` was set to an unsupported value`
|
|
),
|
|
})
|
|
)
|
|
}
|
|
|
|
expect(logs).toContainEqual(
|
|
expect.objectContaining({
|
|
source: 'log',
|
|
message: expect.stringContaining(
|
|
'correct: default submit behavior was not prevented'
|
|
),
|
|
})
|
|
)
|
|
expect(logs).not.toContainEqual(
|
|
expect.objectContaining({
|
|
source: 'log',
|
|
message: expect.stringContaining(
|
|
'incorrect: default submit behavior was prevented'
|
|
),
|
|
})
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
it('does not push a new history entry if `replace` is passed', async () => {
|
|
const session = await next.browser(pathPrefix + `/forms/with-replace`)
|
|
const navigationTracker = await trackMpaNavs(session)
|
|
|
|
// apparently this is usually not 1...?
|
|
const prevHistoryLength: number = await session.eval(`history.length`)
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
await session.waitForElementByCss('#search-results')
|
|
|
|
expect(await navigationTracker.didMpaNavigate()).toBe(false)
|
|
expect(await session.eval(`history.length`)).toEqual(prevHistoryLength)
|
|
})
|
|
|
|
it('does not navigate if preventDefault is called in onSubmit', async () => {
|
|
const session = await next.browser(
|
|
pathPrefix + `/forms/with-onsubmit-preventdefault`
|
|
)
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
// see fixture code for explanation why we expect this
|
|
|
|
await session.waitForElementByCss('#redirected-results')
|
|
expect(new URL(await session.url()).pathname).toEqual(
|
|
pathPrefix + '/redirected-from-action'
|
|
)
|
|
})
|
|
|
|
it('url-encodes file inputs, but warns about them', async () => {
|
|
const session = await next.browser(pathPrefix + `/forms/with-file-input`)
|
|
|
|
const fileInputSelector = 'input[type="file"]'
|
|
// Fake a file to upload
|
|
await session.eval(`
|
|
const fileInput = document.querySelector(${JSON.stringify(fileInputSelector)});
|
|
const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
|
|
const list = new DataTransfer();
|
|
list.items.add(file);
|
|
fileInput.files = list.files;
|
|
`)
|
|
|
|
const searchInput = await session.elementByCss('input[name="query"]')
|
|
await searchInput.fill('my search')
|
|
|
|
const submitButton = await session.elementByCss('[type="submit"]')
|
|
await submitButton.click()
|
|
|
|
if (isNextDev) {
|
|
const logs = await session.log()
|
|
expect(logs).toContainEqual(
|
|
expect.objectContaining({
|
|
source: 'warning',
|
|
message: expect.stringContaining(
|
|
`<Form> only supports file inputs if \`action\` is a function`
|
|
),
|
|
})
|
|
)
|
|
}
|
|
|
|
const result = await session.waitForElementByCss('#search-results').text()
|
|
expect(result).toMatch(/query: "my search"/)
|
|
|
|
const url = new URL(await session.url())
|
|
expect([...url.searchParams.entries()]).toEqual([
|
|
['query', 'my search'],
|
|
['file', 'hello.txt'],
|
|
])
|
|
})
|
|
})
|
|
|
|
async function trackMpaNavs(session: Playwright) {
|
|
const id = Date.now()
|
|
await session.eval(`window.__MPA_NAV_ID = ${id}`)
|
|
return {
|
|
async didMpaNavigate() {
|
|
const maybeId = await session.eval(`window.__MPA_NAV_ID`)
|
|
return id !== maybeId
|
|
},
|
|
}
|
|
}
|
|
}
|