diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue
index 5772b785a1..978c042761 100644
--- a/frontend/src/views/admin/ProxiesView.vue
+++ b/frontend/src/views/admin/ProxiesView.vue
@@ -24,7 +24,7 @@
v-model="filters.protocol"
:options="protocolOptions"
:placeholder="t('admin.proxies.allProtocols')"
- @change="loadProxies"
+ @change="handleFilterChange"
/>
@@ -32,7 +32,7 @@
v-model="filters.status"
:options="statusOptions"
:placeholder="t('admin.proxies.allStatus')"
- @change="loadProxies"
+ @change="handleFilterChange"
/>
@@ -1221,6 +1221,11 @@ const loadProxies = async () => {
}
}
+const handleFilterChange = () => {
+ pagination.page = 1
+ loadProxies()
+}
+
let searchTimeout: ReturnType
const handleSearch = () => {
clearTimeout(searchTimeout)
diff --git a/frontend/src/views/admin/__tests__/ProxiesView.filters.spec.ts b/frontend/src/views/admin/__tests__/ProxiesView.filters.spec.ts
new file mode 100644
index 0000000000..e9088e263a
--- /dev/null
+++ b/frontend/src/views/admin/__tests__/ProxiesView.filters.spec.ts
@@ -0,0 +1,104 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { flushPromises, shallowMount } from '@vue/test-utils'
+import ProxiesView from '../ProxiesView.vue'
+
+const { listProxies, getAllWithCount } = vi.hoisted(() => ({
+ listProxies: vi.fn(),
+ getAllWithCount: vi.fn()
+}))
+
+vi.mock('@/api/admin', () => ({
+ adminAPI: { proxies: { list: listProxies, getAllWithCount } }
+}))
+vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn() }) }))
+vi.mock('vue-i18n', async () => ({
+ ...await vi.importActual('vue-i18n'),
+ useI18n: () => ({ t: (key: string) => key })
+}))
+
+const mountView = () => shallowMount(ProxiesView, {
+ global: {
+ stubs: {
+ AppLayout: { template: '
' },
+ TablePageLayout: {
+ template: '
'
+ },
+ DataTable: {
+ props: ['data'],
+ template: '{{ data.map(row => row.name).join(",") }}
'
+ },
+ Pagination: {
+ props: ['page'],
+ emits: ['update:page'],
+ template: ''
+ },
+ Select: {
+ props: ['modelValue', 'options', 'placeholder'],
+ emits: ['update:modelValue', 'change'],
+ template: ``
+ }
+ }
+ }
+})
+
+let wrapper: ReturnType
+
+beforeEach(() => {
+ vi.clearAllMocks()
+ getAllWithCount.mockResolvedValue([])
+ listProxies.mockResolvedValue({ items: [], total: 100, pages: 5 })
+})
+
+afterEach(() => wrapper?.unmount())
+
+describe('proxy list filter pagination', () => {
+ it.each([
+ ['protocol', 'admin.proxies.allProtocols', 'socks5', ''],
+ ['protocol', 'admin.proxies.allProtocols', '', 'http'],
+ ['status', 'admin.proxies.allStatus', 'expired', ''],
+ ['status', 'admin.proxies.allStatus', '', 'active']
+ ])('starts at page one when changing %s through %s to "%s"', async (field, placeholder, value, initial) => {
+ wrapper = mountView()
+ await flushPromises()
+ const filter = wrapper.get(`select[data-filter="${placeholder}"]`)
+ if (initial) {
+ await filter.setValue(initial)
+ await flushPromises()
+ }
+ await wrapper.get('[data-test="page"]').trigger('click')
+ await flushPromises()
+ expect(listProxies.mock.lastCall?.[0]).toBe(2)
+ const [, pageSize, previousFilters] = listProxies.mock.lastCall!
+ listProxies.mockImplementation(async (page) => ({
+ items: page === 1 ? [{ id: 1, name: 'matching-proxy' }] : [],
+ total: 1,
+ pages: 1
+ }))
+
+ await filter.setValue(value)
+ await flushPromises()
+
+ expect(listProxies).toHaveBeenLastCalledWith(
+ 1, pageSize, { ...previousFilters, [field]: value || undefined },
+ { signal: expect.any(AbortSignal) }
+ )
+ expect(wrapper.get('[data-test="page"]').text()).toBe('1')
+ expect(wrapper.get('[data-test="rows"]').text()).toBe('matching-proxy')
+ })
+
+ it('keeps the current page when refreshing the same filter', async () => {
+ wrapper = mountView()
+ await flushPromises()
+ await wrapper.get('[data-test="page"]').trigger('click')
+ await flushPromises()
+
+ await wrapper.get('button[title="common.refresh"]').trigger('click')
+ await flushPromises()
+
+ expect(listProxies.mock.lastCall?.[0]).toBe(2)
+ expect(wrapper.get('[data-test="page"]').text()).toBe('2')
+ })
+})