From 17d4dd2d4ada7049bcdc0fe7b547e86ed4068964 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Sun, 28 Sep 2025 21:22:02 +0200 Subject: [PATCH 1/4] wip --- .../Pages/InfiniteScroll/Filtering.vue | 52 +++++++ tests/app/eloquent.js | 139 ++++++++++++------ tests/app/server.js | 45 +++++- tests/infinite-scroll.spec.ts | 29 ++++ 4 files changed, 221 insertions(+), 44 deletions(-) create mode 100644 packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue diff --git a/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue new file mode 100644 index 000000000..449890c4f --- /dev/null +++ b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue @@ -0,0 +1,52 @@ + + + diff --git a/tests/app/eloquent.js b/tests/app/eloquent.js index b6e12f0e0..d3ad547e7 100644 --- a/tests/app/eloquent.js +++ b/tests/app/eloquent.js @@ -5,6 +5,102 @@ function makeUser(id) { } } +export function getUserNames() { + return [ + 'Adelle Crona DVM', + 'Alison Walter PhD', + 'Aliza Langosh II', + 'Amara DuBuque', + 'Amaya Lang', + 'Angelica Rodriguez', + 'Anjali Windler', + 'Ansley Gusikowski', + 'Asha Welch II', + 'Bailee Zulauf', + 'Barrett Heathcote', + 'Beryl Morar', + 'Bethany Grant', + 'Braden Mayert II', + 'Breana Herzog', + 'Camylle Metz Sr.', + 'Carmen Kerluke', + 'Casimer McClure', + 'Cecil Walsh', + 'Chandler McKenzie', + 'Chaya Rempel', + 'Chelsey Mertz Jr.', + 'Coralie Auer', + 'Daniella Hoppe', + 'Daphnee Douglas', + 'Davonte Heathcote', + 'Dejah Parisian', + 'Demarco Medhurst', + 'Dewayne Rau', + 'Diamond Gibson PhD', + 'Diamond Herzog', + 'Dino Predovic I', + 'Domingo Luettgen', + 'Dora Runolfsdottir', + 'Dr. Billy Larkin', + 'Dr. Chase Green', + 'Dr. Curtis Lehner', + 'Einar Crona MD', + 'Eloisa Pollich', + 'Elsie Goldner', + 'Emma Little Sr.', + 'Erika Ziemann DDS', + 'Ethan Beatty', + 'Euna Boehm', + 'Euna Kerluke', + 'Felton Yost', + 'Genesis Hand', + 'Hailie Quitzon', + 'Helga Waelchi', + 'Ibrahim Jakubowski', + 'Jack Halvorson', + 'Jasmin Stoltenberg', + 'Jennie Olson PhD', + 'Jimmy Gusikowski', + 'Joy Schimmel', + 'Kamron Bechtelar DDS', + 'Katarina McLaughlin', + 'Katharina Towne', + 'Kavon Sporer', + 'Keshawn Langosh DDS', + 'Lacy Johnston V', + 'Lauren Thiel', + 'Lelia Haley', + 'Lonny Hermiston', + 'Lupe Jacobs', + 'Magdalena Rowe', + 'Marjolaine Gleason', + 'Mattie Bradtke', + 'Miss Amiya Altenwerth', + 'Miss Haven Kuhic', + 'Miss Janie Bayer', + 'Miss Raegan Doyle IV', + 'Molly Murray', + 'Niko Christiansen Jr.', + 'Paxton Koss', + 'Reilly Bechtelar', + 'Rex Blanda', + 'Riley Legros', + 'River Pfeffer', + 'Rory Lubowitz', + 'Rosamond Mueller II', + 'Rosario Nicolas Sr.', + 'Sandrine Hammes', + 'Tad Thompson', + 'Talon Fahey DVM', + 'Taylor Kuhlman IV', + 'Tyler Zieme', + 'Vella Price', + 'Virginie Beatty', + 'Wiley Donnelly', + 'Woodrow Kuvalis', + ] +} + function getUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { // orderByDesc = false // page = 1 => User 1 ... 15, page = 3 => User 31 ... 40 @@ -35,29 +131,6 @@ function getUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { } } -export function simplePaginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { - const users = getUsers(page, perPage, total, orderByDesc) - const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 - - const paginated = { - current_page: page, - data: users, - from: users[0]?.id || null, - per_page: perPage, - to: users[users.length - 1]?.id || null, - } - - return { - paginated, - scrollProp: { - pageName: 'page', - previousPage: page > 1 ? page - 1 : null, - nextPage: hasMore ? page + 1 : null, - currentPage: page, - }, - } -} - export function paginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { const users = getUsers(page, perPage, total, orderByDesc) const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 @@ -78,23 +151,3 @@ export function paginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = }, } } - -export function cursorPaginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { - const users = getUsers(page, perPage, total, orderByDesc) - const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 - - return { - paginated: { - data: users, - per_page: perPage, - next_cursor: hasMore ? page + 1 : null, - prev_cursor: page === 1 ? null : page - 1, - }, - scrollProp: { - pageName: 'page', - previousPage: page > 1 ? page - 1 : null, - nextPage: hasMore ? page + 1 : null, - currentPage: page, - }, - } -} diff --git a/tests/app/server.js b/tests/app/server.js index a57bd42fd..fb155069c 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -4,7 +4,7 @@ const inertia = require('./helpers') const bodyParser = require('body-parser') const multer = require('multer') const { showServerStatus } = require('./server-status') -const { paginateUsers } = require('./eloquent') +const { getUserNames, paginateUsers } = require('./eloquent') const app = express() app.use(bodyParser.urlencoded({ extended: true })) @@ -925,6 +925,49 @@ app.get('/infinite-scroll/short-content', (req, res) => renderInfiniteScroll(req, res, 'InfiniteScroll/ShortContent', 100, false, 5), ) +function renderInfiniteScrollWithTag(req, res, component, total = 40, orderByDesc = false, perPage = 15) {} + +app.get('/infinite-scroll/filtering', (req, res) => { + const filter = req.query.filter + const search = req.query.search + + let users = getUserNames() + + if (search) { + users = users.filter((user) => user.toLowerCase().includes(search.toLowerCase())) + } + + if (filter === 'a-m') { + users = users.filter((user) => user.toLowerCase() >= 'a' && user.toLowerCase() <= 'm') + } else if (filter === 'n-z') { + users = users.filter((user) => user.toLowerCase() >= 'n' && user.toLowerCase() <= 'z') + } + + const perPage = 15 + const page = req.query.page ? parseInt(req.query.page) : 1 + + const partialReload = !!req.headers['x-inertia-partial-data'] + const shouldAppend = req.headers['x-inertia-infinite-scroll-merge-intent'] !== 'prepend' + const { paginated, scrollProp } = paginateUsers(page, perPage, users.length) + + if (page > 1) { + users = users.slice((page - 1) * perPage, page * perPage) + } + + paginated.data = paginated.data.map((user, i) => ({ ...user, name: users[i] })) + + setTimeout( + () => + inertia.render(req, res, { + component: 'InfiniteScroll/Filtering', + props: { users: paginated, filter, search }, + [shouldAppend ? 'mergeProps' : 'prependProps']: ['users.data'], + scrollProps: { users: scrollProp }, + }), + partialReload ? 250 : 0, + ) +}) + app.all('*', (req, res) => inertia.render(req, res)) const adapterPorts = { diff --git a/tests/infinite-scroll.spec.ts b/tests/infinite-scroll.spec.ts index ea1e95099..dfcfea880 100644 --- a/tests/infinite-scroll.spec.ts +++ b/tests/infinite-scroll.spec.ts @@ -1728,3 +1728,32 @@ Object.entries({ }) }) }) + +test.describe('Query parameter handling', () => { + test('it keeps the existing query parameters intact when updating the page param', async ({ page }) => { + await page.goto('/infinite-scroll/filtering') + + // Click N-Z filter + // See 'Niko Christiansen Jr.' as first user + // Scroll to bottom to load page 2 + // Assert filter=n-z&page=2 + // See 'Nike Christiansen Jr.' still as first user + // See 'Woodrow Kuvalis' as last user + }) + + test('it resets the infinite scroll component when navigating to the same page with different query params', async ({ + page, + }) => { + await page.goto('/infinite-scroll/filtering') + + // Click A-M filter + // See 'Adelle Crona DVM' as first user and 'Breana Herzog' as last user + // Scroll to bottom to load page 2 + // See 'Camylle Metz Sr.' as 16th user + // Click N-Z filter + // See 'Niko Christiansen Jr.' as first + // Scroll to bottom to load page 2 + // See 'Nike Christiansen Jr.' as first user + // See 'Woodrow Kuvalis' as last user + }) +}) From 8abd968b99c8afd39c8e2e7bc7504dcfdf1e0ea5 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Sun, 28 Sep 2025 21:34:48 +0200 Subject: [PATCH 2/4] wip --- .../Pages/InfiniteScroll/Filtering.vue | 3 +- tests/infinite-scroll.spec.ts | 83 ++++++++++++++++--- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue index 449890c4f..05ef41cd5 100644 --- a/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue +++ b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue @@ -12,12 +12,13 @@ const props = defineProps<{ const form = useForm({ filter: undefined, + page: undefined, search: props.search, }) watch( () => form.search, - debounce(() => form.get(''), 250), + debounce(() => form.get('', { replace: true }), 250), ) diff --git a/tests/infinite-scroll.spec.ts b/tests/infinite-scroll.spec.ts index dfcfea880..b28a7e75c 100644 --- a/tests/infinite-scroll.spec.ts +++ b/tests/infinite-scroll.spec.ts @@ -1731,29 +1731,86 @@ Object.entries({ test.describe('Query parameter handling', () => { test('it keeps the existing query parameters intact when updating the page param', async ({ page }) => { + requests.listen(page) await page.goto('/infinite-scroll/filtering') - // Click N-Z filter - // See 'Niko Christiansen Jr.' as first user + await page.getByRole('link', { name: 'N-Z' }).first().click() + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + // Scroll to bottom to load page 2 + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + // Assert filter=n-z&page=2 - // See 'Nike Christiansen Jr.' still as first user - // See 'Woodrow Kuvalis' as last user + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await expect(infiniteScrollRequests().length).toBe(1) }) test('it resets the infinite scroll component when navigating to the same page with different query params', async ({ page, }) => { + requests.listen(page) await page.goto('/infinite-scroll/filtering') - // Click A-M filter - // See 'Adelle Crona DVM' as first user and 'Breana Herzog' as last user - // Scroll to bottom to load page 2 - // See 'Camylle Metz Sr.' as 16th user - // Click N-Z filter - // See 'Niko Christiansen Jr.' as first - // Scroll to bottom to load page 2 - // See 'Nike Christiansen Jr.' as first user - // See 'Woodrow Kuvalis' as last user + await page.getByRole('link', { name: 'A-M' }).first().click() + + await expect(page.getByText('Adelle Crona DVM')).toBeVisible() + await expect(page.getByText('Breana Herzog')).toBeVisible() + await expect(page.getByText('Current filter: a-m').first()).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Camylle Metz Sr.')).toBeVisible() + + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight - 1000)) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=a-m') + expect(page.url()).toContain('page=2') + + await expect(infiniteScrollRequests().length).toBe(1) + + await page.getByRole('link', { name: 'N-Z' }).first().click() + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + + await expect(page.getByText('Adelle Crona DVM')).toBeHidden() + await expect(page.getByText('Camylle Metz Sr.')).toBeHidden() + + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(infiniteScrollRequests().length).toBe(2) + }) + + test('it reset the page and filter params when searching for a user', async ({ page }) => { + requests.listen(page) + await page.goto('/infinite-scroll/filtering') + + // Click N-Z + // Scroll to bottom, load next page + // Assert filter=n-z&page=2 + // Search for 'adelle' in bottom input box + // Assert filter=adelle (no page param, no filter param) + // Assert only 'Adelle Crona DVM' is visible + // Click N-Z again + // Assert filter=n-z (no page param, no filter param) + // Assert 'Adelle Crona DVM' is hidden, 'Niko Christiansen Jr.' is visible + // Scroll to bottom, load next page + // Assert filter=n-z&page=2 + // Assert 'Niko Christiansen Jr.' and 'Woodrow Kuvalis' are visible }) }) From e78011c8a73a0b55acc69b7e2d9a45b2cdd765d4 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Sun, 28 Sep 2025 21:38:06 +0200 Subject: [PATCH 3/4] Update infinite-scroll.spec.ts --- tests/infinite-scroll.spec.ts | 60 +++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/tests/infinite-scroll.spec.ts b/tests/infinite-scroll.spec.ts index b28a7e75c..2101665f4 100644 --- a/tests/infinite-scroll.spec.ts +++ b/tests/infinite-scroll.spec.ts @@ -1799,18 +1799,60 @@ test.describe('Query parameter handling', () => { test('it reset the page and filter params when searching for a user', async ({ page }) => { requests.listen(page) await page.goto('/infinite-scroll/filtering') + await page.getByRole('link', { name: 'N-Z' }).first().click() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') - // Click N-Z - // Scroll to bottom, load next page - // Assert filter=n-z&page=2 // Search for 'adelle' in bottom input box - // Assert filter=adelle (no page param, no filter param) + await page.locator('input').nth(1).fill('adelle') + + await expect(page.getByText('Adelle Crona DVM')).toBeVisible() + + // Assert search=adelle (no page param, no filter param) + expect(page.url()).toContain('search=adelle') + expect(page.url()).not.toContain('page=') + expect(page.url()).not.toContain('filter=') + await expect(page.getByText('Current search: adelle').first()).toBeVisible() + await expect(page.getByText('Current filter: none').first()).toBeVisible() + // Assert only 'Adelle Crona DVM' is visible - // Click N-Z again - // Assert filter=n-z (no page param, no filter param) + await expect(page.getByText('Niko Christiansen Jr.')).toBeHidden() + await expect(page.getByText('Woodrow Kuvalis')).toBeHidden() + + // Click N-Z again (this should reset search and apply filter) + await page.getByRole('link', { name: 'N-Z' }).first().click() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + + // Assert filter=n-z (no page param, no search param) + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + expect(page.url()).not.toContain('search=') + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + await expect(page.getByText('Current search: none').first()).toBeVisible() + // Assert 'Adelle Crona DVM' is hidden, 'Niko Christiansen Jr.' is visible - // Scroll to bottom, load next page - // Assert filter=n-z&page=2 - // Assert 'Niko Christiansen Jr.' and 'Woodrow Kuvalis' are visible + await expect(page.getByText('Adelle Crona DVM')).toBeHidden() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await expect(infiniteScrollRequests().length).toBe(2) // Initial N-Z page 2 load, final N-Z page 2 load }) }) From 6fcdd7aecbebf49b3bddae767825911c85d460ac Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Sun, 28 Sep 2025 21:49:15 +0200 Subject: [PATCH 4/4] react/svelte --- .../Pages/InfiniteScroll/Filtering.tsx | 67 +++++++++++++++++++ .../Pages/InfiniteScroll/Filtering.svelte | 64 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx create mode 100644 packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte diff --git a/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx b/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx new file mode 100644 index 000000000..3a4ff2207 --- /dev/null +++ b/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx @@ -0,0 +1,67 @@ +import { InfiniteScroll, Link, useForm } from '@inertiajs/react' +import { debounce } from 'lodash-es' +import { useCallback, useEffect } from 'react' +import UserCard, { User } from './UserCard' + +interface Props { + users: { data: User[] } + filter?: string + search?: string +} + +export default ({ users, filter, search }: Props) => { + const { data, setData, get } = useForm({ + filter: undefined, + page: undefined, + search: search, + }) + + const debouncedSearch = useCallback( + debounce(() => { + get('', { replace: true }) + }, 250), + [get], + ) + + useEffect(() => { + if (data.search !== search) { + debouncedSearch() + } + }, [data.search, search, debouncedSearch]) + + const handleSearchChange = (e: React.ChangeEvent) => { + setData('search', e.target.value) + } + + return ( +
+
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+ +
Loading...
} + > + {users.data.map((user) => ( + + ))} +
+ +
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+
+ ) +} diff --git a/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte b/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte new file mode 100644 index 000000000..cdc36b396 --- /dev/null +++ b/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte @@ -0,0 +1,64 @@ + + +
+
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+ + +
Loading...
+ + {#each users.data as user (user.id)} + + {/each} +
+ +
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+