Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/fix-fragment-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'houdini': patch
'houdini-react': patch
'houdini-svelte': patch
'houdini-core': patch
---

fixed fragment pagination
6 changes: 0 additions & 6 deletions .changeset/singlepage-cursor-stack.md

This file was deleted.

24 changes: 17 additions & 7 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
benchmark:
name: Benchmark
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
permissions:
contents: read
steps:
Expand All @@ -36,12 +36,18 @@ jobs:
# ── PR branch ────────────────────────────────────────────────────
- run: pnpm install --frozen-lockfile --prefer-offline

- name: Benchmark PR branch
run: BENCH_MAX_N=1000 npx vitest bench packages/houdini/src/runtime/cache/benchmarks/ --outputJson /tmp/benchmark.current.json
- name: Benchmark PR branch (3 runs)
run: |
for i in 1 2 3; do
BENCH_MAX_N=1000 npx vitest bench packages/houdini/src/runtime/cache/benchmarks/ --outputJson /tmp/benchmark.current.$i.json
done
node perf/merge.js /tmp/benchmark.current.1.json /tmp/benchmark.current.2.json /tmp/benchmark.current.3.json > /tmp/benchmark.current.json

# ── Base branch ──────────────────────────────────────────────────
- name: Save benchmark suite from PR
run: cp -r packages/houdini/src/runtime/cache/benchmarks /tmp/houdini-benchmarks
- name: Save benchmark suite and merge script from PR
run: |
cp -r packages/houdini/src/runtime/cache/benchmarks /tmp/houdini-benchmarks
cp perf/merge.js /tmp/houdini-merge.js

- name: Checkout base branch
run: git checkout ${{ github.base_ref }}
Expand All @@ -53,8 +59,12 @@ jobs:

- run: pnpm install --frozen-lockfile --prefer-offline

- name: Benchmark base branch
run: BENCH_MAX_N=1000 npx vitest bench packages/houdini/src/runtime/cache/benchmarks/ --outputJson /tmp/benchmark.baseline.json
- name: Benchmark base branch (3 runs)
run: |
for i in 1 2 3; do
BENCH_MAX_N=1000 npx vitest bench packages/houdini/src/runtime/cache/benchmarks/ --outputJson /tmp/benchmark.baseline.$i.json
done
node /tmp/houdini-merge.js /tmp/benchmark.baseline.1.json /tmp/benchmark.baseline.2.json /tmp/benchmark.baseline.3.json > /tmp/benchmark.baseline.json

# ── Compare ──────────────────────────────────────────────────────
- name: Checkout PR branch
Expand Down
1 change: 1 addition & 0 deletions e2e/kit/src/lib/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const routes = {
Pagination_fragment_bidirectional_cursor: '/pagination/fragment/bidirectional-cursor',
Pagination_fragment_offset: '/pagination/fragment/offset',
Pagination_fragment_required_arguments: '/pagination/fragment/required-arguments',
Pagination_fragment_forward_cursor_singlepage: '/pagination/fragment/forward-cursor-singlepage',

nested_argument_fragments: '/nested-argument-fragments',
nested_argument_fragments_masking: '/nested-argument-fragments-masking',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
import { paginatedFragment, graphql } from '$houdini'
import type { PageData } from './$types'
import { stringify } from '$lib/utils/stringify'
export let data: PageData
$: ({ UserFragmentForwardsCursorSinglePageQuery: queryResult } = data)
$: fragmentResult = paginatedFragment(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$queryResult.data?.user ?? null,
graphql(`
fragment ForwardsCursorSinglePageFragment on User {
usersConnectionSnapshot(snapshot: "pagination-fragment-forwards-cursor-singlepage-svelte", first: 2) @paginate(mode: SinglePage) {
edges {
node {
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`)
)
</script>

<div id="result">
{$fragmentResult.data?.usersConnectionSnapshot.edges.map(({ node }) => node?.name).join(', ')}
</div>

<div id="pageInfo">
{stringify($fragmentResult.pageInfo)}
</div>

<button id="previous" on:click={() => fragmentResult.loadPreviousPage()}>previous</button>
<button id="next" on:click={() => fragmentResult.loadNextPage()}>next</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { PageLoad } from './$types'
import { graphql } from '$houdini'

const store = graphql(`
query UserFragmentForwardsCursorSinglePageQuery {
user(id: "1", snapshot: "pagination-fragment-forwards-cursor-singlepage-svelte") {
...ForwardsCursorSinglePageFragment
}
}
`)

export const load: PageLoad = async (event) => {
await store.fetch({ event })

return {
UserFragmentForwardsCursorSinglePageQuery: store,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test } from '@playwright/test'
import { routes } from '../../../../lib/utils/routes.js'
import {
expect_0_gql,
expect_1_gql,
expect_to_be,
expectToContain,
goto,
} from '../../../../lib/utils/testsHelper.js'

test.describe('forwards cursor fragment single page', () => {
test('loadNextPage replaces data', async ({ page }) => {
await goto(page, routes.Pagination_fragment_forward_cursor_singlepage)

await expect_to_be(page, 'Bruce Willis, Samuel Jackson')
await expectToContain(page, `"hasPreviousPage":false`)
await expectToContain(page, `"hasNextPage":true`)

await expect_1_gql(page, 'button[id=next]')
await expect_to_be(page, 'Morgan Freeman, Tom Hanks')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

await expect_1_gql(page, 'button[id=next]')
await expect_to_be(page, 'Will Smith, Harrison Ford')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

await expect_0_gql(page, 'button[id=previous]')
await expect_to_be(page, 'Morgan Freeman, Tom Hanks')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

await expect_0_gql(page, 'button[id=previous]')
await expect_to_be(page, 'Bruce Willis, Samuel Jackson')
await expectToContain(page, `"hasPreviousPage":false`)
await expectToContain(page, `"hasNextPage":true`)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { routes } from '../../../../lib/utils/routes.js'
import {
expect_to_be,
expectToContain,
expect_0_gql,
expect_1_gql,
goto,
stringify,
Expand All @@ -29,8 +30,8 @@ test.describe('bidirectional cursor single page paginated query', () => {

/// Click on the next button

// load the next page and wait for the response
await expect_1_gql(page, 'button[id=next]')
// page 2 was the initial load — cache hit, no network request
await expect_0_gql(page, 'button[id=next]')

// there should be no previous page
await expectToContain(page, `"hasPreviousPage":true`)
Expand Down Expand Up @@ -87,8 +88,8 @@ test.describe('bidirectional cursor single page paginated query', () => {

/// Click on the previous button

// load the previous page and wait for the response
await expect_1_gql(page, 'button[id=previous]')
// page 2 was the initial load — cache hit, no network request
await expect_0_gql(page, 'button[id=previous]')

// make sure we got the new content
await expect_to_be(page, 'Morgan Freeman, Tom Hanks')
Expand All @@ -100,7 +101,7 @@ test.describe('bidirectional cursor single page paginated query', () => {

/// Click on the previous button

// load the previous page and wait for the response
// previousCursors now empty — use before cursor to fetch page 1
await expect_1_gql(page, 'button[id=previous]')

// make sure we got the new content
Expand Down
4 changes: 0 additions & 4 deletions e2e/react/src/+index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ class ErrorBoundary extends React.Component {
return { hasError: true }
}

componentDidCatch(error, info) {
console.error('ErrorBoundary caught an error:', error, info)
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query FragmentCursorBackwardsSinglePageQuery {
user(id: "1", snapshot: "pagination-fragment-cursor-backwards-singlepage") {
...FragmentCursorBackwardsSinglePageFragment
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { graphql, useFragmentHandle } from '$houdini'
import type { PageProps } from './$types'

const fragment = graphql(`
fragment FragmentCursorBackwardsSinglePageFragment on User {
usersConnectionSnapshot(
snapshot: "pagination-fragment-cursor-backwards-singlepage"
last: 2
) @paginate(mode: SinglePage) {
edges {
node {
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`)

export default function ({ FragmentCursorBackwardsSinglePageQuery }: PageProps) {
const handle = useFragmentHandle(FragmentCursorBackwardsSinglePageQuery.user, fragment)

return (
<>
<div id="result">
{handle.data?.usersConnectionSnapshot.edges.map(({ node }) => node?.name).join(', ')}
</div>

<div id="pageInfo">{JSON.stringify(handle.pageInfo)}</div>

<button id="previous" onClick={() => handle.loadPrevious()}>
previous
</button>

<button id="next" onClick={() => handle.loadNext()}>
next
</button>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from '@playwright/test'
import { routes } from '~/utils/routes.js'
import {
expect_to_be,
expectToContain,
expect_0_gql,
expect_1_gql,
goto,
} from '~/utils/testsHelper.js'

test.describe('backwards cursor fragment single page paginated query', () => {
test('loadPreviousPage replaces data then loadNextPage navigates forward', async ({ page }) => {
await goto(page, routes.pagination_fragment_cursor_backwards_singlepage)

await expect_to_be(page, 'Eddie Murphy, Clint Eastwood')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":false`)

await expect_1_gql(page, 'button[id=previous]')
await expect_to_be(page, 'Will Smith, Harrison Ford')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

await expect_1_gql(page, 'button[id=previous]')
await expect_to_be(page, 'Morgan Freeman, Tom Hanks')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

// "Will Smith, Harrison Ford" was fetched on the way back — served from cache.
await expect_0_gql(page, 'button[id=next]')
await expect_to_be(page, 'Will Smith, Harrison Ford')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":true`)

// Page 4 was the initial load — cache hit, no network request.
await expect_0_gql(page, 'button[id=next]')
await expect_to_be(page, 'Eddie Murphy, Clint Eastwood')
await expectToContain(page, `"hasPreviousPage":true`)
await expectToContain(page, `"hasNextPage":false`)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query FragmentCursorBackwardsQuery {
user(id: "1", snapshot: "pagination-fragment-cursor-backwards") {
...FragmentCursorBackwardsFragment
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { graphql, useFragmentHandle } from '$houdini'
import type { PageProps } from './$types'

const fragment = graphql(`
fragment FragmentCursorBackwardsFragment on User {
usersConnectionSnapshot(snapshot: "pagination-fragment-cursor-backwards", last: 2) @paginate {
edges {
node {
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`)

export default function ({ FragmentCursorBackwardsQuery }: PageProps) {
const handle = useFragmentHandle(FragmentCursorBackwardsQuery.user, fragment)

return (
<>
<div id="result">
{handle.data?.usersConnectionSnapshot.edges.map(({ node }) => node?.name).join(', ')}
</div>

<div id="pageInfo">{JSON.stringify(handle.pageInfo)}</div>

<button id="previous" onClick={() => handle.loadPrevious()}>
previous
</button>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from '@playwright/test'
import { routes } from '~/utils/routes.js'
import { expect_to_be, expectToContain, expect_1_gql, goto } from '~/utils/testsHelper.js'

test.describe('backwards cursor fragment paginated query', () => {
test('loadPreviousPage prepends data', async ({ page }) => {
await goto(page, routes.pagination_fragment_cursor_backwards)

await expect_to_be(page, 'Eddie Murphy, Clint Eastwood')
await expectToContain(page, `"hasPreviousPage":true`)

await expect_1_gql(page, 'button[id=previous]')
await expect_to_be(page, 'Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood')

await expect_1_gql(page, 'button[id=previous]')
await expect_to_be(
page,
'Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood'
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query FragmentSinglePageQuery {
user(id: "1", snapshot: "pagination-fragment-bidirectional-singlepage") {
...UserConnectionSinglePageFragment
}
}
Loading
Loading