diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86ab9627..ad154eb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,3 +52,40 @@ jobs: - name: Generated API reference is up to date run: git diff --exit-code packages/docs/api + + test-vue-rc: + runs-on: ubuntu-latest + name: Test against Vue release candidate + + steps: + - name: Set alternate npm integrity keys + run: | + echo COREPACK_INTEGRITY_KEYS="$(curl https://registry.npmjs.org/-/npm/v1/keys | jq -c '{npm: .keys}')" >> $GITHUB_ENV + - uses: actions/checkout@v4 + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Resolve the Vue release candidate + run: | + VUE_RC="$(npm view vue dist-tags.rc)" + echo "Testing against Vue $VUE_RC" + # Pinned to the resolved version rather than the `rc` tag so every package in the + # tree lands on one Vue — two copies mean two reactive graphs and bogus failures. + npm pkg set \ + "pnpm.overrides.vue=$VUE_RC" \ + "pnpm.overrides.@vue/reactivity=$VUE_RC" \ + "pnpm.overrides.@vue/runtime-core=$VUE_RC" \ + "pnpm.overrides.@vue/runtime-dom=$VUE_RC" \ + "pnpm.overrides.@vue/server-renderer=$VUE_RC" \ + "pnpm.overrides.@vue/shared=$VUE_RC" + + - run: pnpm install --no-frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Tests + run: pnpm run test diff --git a/packages/vue-apollo-composable/src/useQuery.test.ts b/packages/vue-apollo-composable/src/useQuery.test.ts index 5f2d6bfb..c19db4cb 100644 --- a/packages/vue-apollo-composable/src/useQuery.test.ts +++ b/packages/vue-apollo-composable/src/useQuery.test.ts @@ -634,6 +634,45 @@ describe('useQuery', () => { wrapper.unmount() }) + + it('should start with the resolved variables when enabled and variables land in the same tick', async () => { + const spy = vi.spyOn(apolloClient, 'watchQuery') + + const TestComponent = defineComponent({ + setup() { + const open = ref(false) + const { current, variables } = useQuery(ECHO_QUERY, () => + !open.value + ? { enabled: false } + : { variables: { message: 'hello' }, fetchPolicy: 'no-cache' as const }) + + return { current, variables, open } + }, + render() { + return h('div', this.current.resultState === 'complete' ? this.current.result.echo : 'no result') + }, + }) + + const wrapper = mount(TestComponent, { + global: { provide: { [DefaultApolloClient]: apolloClient } }, + }) + + expect(spy).not.toHaveBeenCalled() + + // One write turns the query on and gives it its variables. + wrapper.vm.open = true + + expect(spy.mock.calls[0]?.[0].variables).toEqual({ message: 'hello' }) + expect(wrapper.vm.variables).toEqual({ message: 'hello' }) + expect(wrapper.vm.current.loading).toBe(true) + + await until(() => wrapper.vm.current.resultState).toBe('complete', { timeout: 200 }) + expect(wrapper.find('div').text()).toBe('hello') + expect(wrapper.vm.current.loading).toBe(false) + + spy.mockRestore() + wrapper.unmount() + }) // #endregion // #region Lifecycle diff --git a/packages/vue-apollo-composable/src/useQuery.ts b/packages/vue-apollo-composable/src/useQuery.ts index 35662836..bcbe188e 100644 --- a/packages/vue-apollo-composable/src/useQuery.ts +++ b/packages/vue-apollo-composable/src/useQuery.ts @@ -1064,8 +1064,15 @@ export function useQueryImpl< return result as TVariables }) + /** One write, so a re-entrant sync watcher cannot catch half a commit (Vue 3.6). */ + const commitState = shallowRef({ variables: variables.value, committing: false }) + /** The actual variables sent to Apollo (may be delayed by debounce/throttle) */ - const currentVariables = shallowRef(variables.value) + const currentVariables = computed({ + get: () => commitState.value.variables, + // Writable for compat's `load(document, variables)`, which pushes variables in by hand. + set: variables => commitState.value = { ...commitState.value, variables }, + }) /** * `true` while `variables` have moved ahead of what has actually been handed to Apollo, @@ -1083,7 +1090,10 @@ export function useQueryImpl< * Bridges the gap between `pending` clearing (on write) and `loading` being set on the * next flush, when the watcher below reobserves. */ - const isCommitting = ref(false) + const isCommitting = computed({ + get: () => commitState.value.committing, + set: committing => commitState.value = { ...commitState.value, committing }, + }) function commitVariables(newVariables: TVariables) { if (equal(newVariables, currentVariables.value)) { @@ -1092,8 +1102,7 @@ export function useQueryImpl< return } - isCommitting.value = true - currentVariables.value = newVariables + commitState.value = { variables: newVariables, committing: true } } const setDebouncedVariables = useDebounceFn(