@blueprint-ts/core 4.0.0-beta.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/docs/services/pagination/index.md +1 -1
  3. package/docs/services/pagination/updating-rows.md +16 -0
  4. package/package.json +1 -1
  5. package/release-tool.json +1 -1
  6. package/src/pagination/BasePaginator.ts +19 -0
  7. package/src/requests/BaseRequest.ts +2 -1
  8. package/src/vue/forms/validation/rules/EmailRule.ts +1 -1
  9. package/tests/service/bulkRequests/BulkRequestSender.test.ts +76 -0
  10. package/tests/service/bulkRequests/BulkRequestWrapper.test.ts +51 -0
  11. package/tests/service/pagination/BasePaginator.test.ts +100 -0
  12. package/tests/service/pagination/InfiniteScroller.test.ts +101 -0
  13. package/tests/service/pagination/PageAwarePaginator.test.ts +133 -0
  14. package/tests/service/pagination/StatePaginator.test.ts +76 -0
  15. package/tests/service/pagination/VueViewDrivers.test.ts +46 -0
  16. package/tests/service/pagination/dtos/StatePaginationDataDto.test.ts +14 -0
  17. package/tests/service/persistenceDrivers/PersistenceDrivers.test.ts +56 -0
  18. package/tests/service/requests/BaseRequest.test.ts +199 -0
  19. package/tests/service/requests/BodiesAndFactories.test.ts +28 -0
  20. package/tests/service/requests/Enums.test.ts +19 -0
  21. package/tests/service/requests/ErrorHandler.test.ts +45 -1
  22. package/tests/service/requests/RequestErrorRouter.test.ts +44 -0
  23. package/tests/service/requests/Responses.test.ts +83 -0
  24. package/tests/service/requests/exceptions/Exceptions.test.ts +43 -0
  25. package/tests/service/requests/fetch/FetchDriver.test.ts +76 -0
  26. package/tests/service/requests/fetch/FetchResponse.test.ts +21 -0
  27. package/tests/service/support/DeferredPromise.test.ts +40 -0
  28. package/tests/service/support/helpers.test.ts +37 -0
  29. package/tests/vue/composables/useConfirmDialog.test.ts +77 -0
  30. package/tests/vue/composables/useGlobalCheckbox.test.ts +126 -0
  31. package/tests/vue/composables/useIsEmpty.test.ts +18 -0
  32. package/tests/vue/composables/useIsOpen.test.ts +25 -0
  33. package/tests/vue/composables/useIsOpenFromVar.test.ts +22 -0
  34. package/tests/vue/composables/useModelWrapper.test.ts +30 -0
  35. package/tests/vue/composables/useOnOpen.test.ts +26 -0
  36. package/tests/vue/forms/PropertyAwareArray.test.ts +30 -0
  37. package/tests/vue/forms/validation/ValidationRules.test.ts +79 -0
  38. package/tests/vue/requests/VueRequestLoaders.test.ts +48 -0
  39. package/tests/vue/router/routeResourceBinding/RouteResourceUtils.test.ts +70 -0
  40. package/tests/vue/state/State.test.ts +151 -0
  41. package/vitest.config.ts +10 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v4.0.0 - 2026-03-01
2
+
3
+ # [4.0.0](/compare/v4.0.0-beta.10...v4.0.0) (2026-03-01)
4
+ ## v4.0.0-beta.10 - 2026-02-28 (beta)
5
+
6
+ # [4.0.0-beta.10](/compare/v4.0.0-beta.9...v4.0.0-beta.10) (2026-02-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * **pagination:** support local row removal without reload 3a66689
12
+ ## v4.0.0-beta.9 - 2026-02-28 (beta)
13
+
14
+ # [4.0.0-beta.9](/compare/v4.0.0-beta.8...v4.0.0-beta.9) (2026-02-28)
1
15
  ## v4.0.0-beta.8 - 2026-02-28 (beta)
2
16
 
3
17
  # [4.0.0-beta.8](/compare/v4.0.0-beta.7...v4.0.0-beta.8) (2026-02-28)
@@ -38,7 +38,7 @@ If you are using Vue, the library provides `VuePaginationDriverFactory` and `Vue
38
38
  - `flush`: clears existing data before applying the next page
39
39
  - `replace`: replaces existing data instead of appending (useful for infinite scroll)
40
40
 
41
- `updateRows` is available on all paginators. See [Updating Rows](./updating-rows).
41
+ `updateRows` and `removeRows` are available on all paginators. See [Updating Rows](./updating-rows).
42
42
 
43
43
  ## Using Laravel Pagination
44
44
 
@@ -34,3 +34,19 @@ const updated = paginator.updateRows(
34
34
  (row) => ({ ...row, updatedAt: new Date().toISOString() })
35
35
  )
36
36
  ```
37
+
38
+ ## Removing Rows
39
+
40
+ Use `removeRows` to delete items from the current page data without reloading. By default it also decrements `total`
41
+ by the number of removed rows. Set `adjustTotal: false` to skip that behavior.
42
+
43
+ ```typescript
44
+ // Remove a single item
45
+ const removed = paginator.removeRows((row) => row.id === targetId)
46
+
47
+ // Remove all drafts without adjusting total
48
+ const removedDrafts = paginator.removeRows(
49
+ (row) => row.status === 'draft',
50
+ { adjustTotal: false }
51
+ )
52
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprint-ts/core",
3
- "version": "4.0.0-beta.8",
3
+ "version": "4.0.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/release-tool.json CHANGED
@@ -20,7 +20,7 @@
20
20
  "subdir": "",
21
21
  "commit_message": "docs: publish {tag}",
22
22
  "add_nojekyll": true,
23
- "force_push": false
23
+ "force_push": true
24
24
  }
25
25
  }
26
26
  }
@@ -55,6 +55,25 @@ export abstract class BasePaginator<ResourceInterface, ViewDriver extends BaseVi
55
55
  return updated
56
56
  }
57
57
 
58
+ public removeRows(
59
+ predicate: (row: ResourceInterface, index: number, data: ResourceInterface[]) => boolean,
60
+ options?: { adjustTotal?: boolean }
61
+ ): number {
62
+ const data = this.viewDriver.getData()
63
+ const next = data.filter((row, index) => !predicate(row, index, data))
64
+ const removed = data.length - next.length
65
+
66
+ if (removed > 0) {
67
+ this.viewDriver.setData(next)
68
+
69
+ if (options?.adjustTotal ?? true) {
70
+ this.viewDriver.setTotal(Math.max(0, this.viewDriver.getTotal() - removed))
71
+ }
72
+ }
73
+
74
+ return removed
75
+ }
76
+
58
77
  public getTotal(): number {
59
78
  return this.viewDriver.getTotal()
60
79
  }
@@ -113,7 +113,8 @@ export abstract class BaseRequest<
113
113
  }
114
114
 
115
115
  public buildUrl(): URL {
116
- const url = this.params !== undefined && Object.keys(this.params).length === 0 ? this.url() : this.url() + '?' + qs.stringify(this.params)
116
+ const hasParams = this.params !== undefined && Object.keys(this.params).length > 0
117
+ const url = hasParams ? this.url() + '?' + qs.stringify(this.params) : this.url()
117
118
 
118
119
  return new URL(url, this.baseUrl() ?? BaseRequest.defaultBaseUrl)
119
120
  }
@@ -14,7 +14,7 @@ export class EmailRule<FormBody extends object> extends BaseRule<FormBody> {
14
14
  return false
15
15
  }
16
16
 
17
- return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)
17
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
18
18
  }
19
19
 
20
20
  public getMessage(): string {
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { BulkRequestSender, BulkRequestExecutionMode } from '../../../src/bulkRequests/BulkRequestSender'
3
+ import { BulkRequestWrapper } from '../../../src/bulkRequests/BulkRequestWrapper'
4
+ import { BulkRequestEventEnum } from '../../../src/bulkRequests/BulkRequestEvent.enum'
5
+
6
+ const createRequest = (options: { failTimes?: number } = {}) => {
7
+ let calls = 0
8
+
9
+ return {
10
+ send: vi.fn().mockImplementation(() => {
11
+ calls += 1
12
+ if (options.failTimes && calls <= options.failTimes) {
13
+ return Promise.reject(new Error('fail'))
14
+ }
15
+ return Promise.resolve('ok')
16
+ }),
17
+ isLoading: vi.fn().mockReturnValue(false),
18
+ setAbortSignal: vi.fn(),
19
+ }
20
+ }
21
+
22
+ describe('BulkRequestSender', () => {
23
+ it('sends requests in parallel and emits success events', async () => {
24
+ const wrappers = [new BulkRequestWrapper(createRequest() as any), new BulkRequestWrapper(createRequest() as any)]
25
+
26
+ const sender = new BulkRequestSender(wrappers, BulkRequestExecutionMode.PARALLEL)
27
+ const onSuccess = vi.fn()
28
+
29
+ sender.on(BulkRequestEventEnum.REQUEST_SUCCESSFUL, onSuccess)
30
+
31
+ const result = await sender.send()
32
+
33
+ expect(onSuccess).toHaveBeenCalledTimes(2)
34
+ expect(result.getSuccessCount()).toBe(2)
35
+ expect(result.getErrorCount()).toBe(0)
36
+ expect(result.getSuccessfulResponses()).toEqual(['ok', 'ok'])
37
+ })
38
+
39
+ it('retries failed requests and emits failure events when still failing', async () => {
40
+ const failingRequest = createRequest({ failTimes: 2 })
41
+ const wrapper = new BulkRequestWrapper(failingRequest as any)
42
+
43
+ const sender = new BulkRequestSender([wrapper], BulkRequestExecutionMode.SEQUENTIAL, 1)
44
+ const onFailed = vi.fn()
45
+
46
+ sender.on(BulkRequestEventEnum.REQUEST_FAILED, onFailed)
47
+
48
+ await sender.send()
49
+
50
+ expect(failingRequest.send).toHaveBeenCalledTimes(2)
51
+ expect(onFailed).toHaveBeenCalledTimes(1)
52
+ })
53
+
54
+ it('reports loading state when any request is loading', () => {
55
+ const request1 = createRequest()
56
+ const request2 = createRequest()
57
+ request2.isLoading = vi.fn().mockReturnValue(true)
58
+
59
+ const sender = new BulkRequestSender(
60
+ [new BulkRequestWrapper(request1 as any), new BulkRequestWrapper(request2 as any)],
61
+ BulkRequestExecutionMode.PARALLEL
62
+ )
63
+
64
+ expect(sender.isLoading).toBe(true)
65
+ })
66
+
67
+ it('removes event handlers with off()', () => {
68
+ const sender = new BulkRequestSender([], BulkRequestExecutionMode.PARALLEL)
69
+ const handler = vi.fn()
70
+
71
+ sender.on(BulkRequestEventEnum.REQUEST_SUCCESSFUL, handler)
72
+ sender.off(BulkRequestEventEnum.REQUEST_SUCCESSFUL)
73
+
74
+ expect((sender as any).events.has(BulkRequestEventEnum.REQUEST_SUCCESSFUL)).toBe(false)
75
+ })
76
+ })
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { BulkRequestWrapper } from '../../../src/bulkRequests/BulkRequestWrapper'
3
+
4
+ const createRequest = (behavior: { shouldFail?: boolean } = {}) => {
5
+ return {
6
+ send: vi.fn().mockImplementation(() => {
7
+ if (behavior.shouldFail) {
8
+ return Promise.reject(new Error('fail'))
9
+ }
10
+ return Promise.resolve('ok')
11
+ }),
12
+ isLoading: vi.fn().mockReturnValue(false),
13
+ setAbortSignal: vi.fn(),
14
+ }
15
+ }
16
+
17
+ describe('BulkRequestWrapper', () => {
18
+ it('stores response on success', async () => {
19
+ const request = createRequest()
20
+ const wrapper = new BulkRequestWrapper(request as any)
21
+
22
+ await wrapper.send()
23
+
24
+ expect(wrapper.getResponse()).toBe('ok')
25
+ expect(wrapper.getError()).toBeNull()
26
+ expect(wrapper.hasError()).toBe(false)
27
+ expect(wrapper.wasSent()).toBe(true)
28
+ })
29
+
30
+ it('stores error on failure', async () => {
31
+ const request = createRequest({ shouldFail: true })
32
+ const wrapper = new BulkRequestWrapper(request as any)
33
+
34
+ await wrapper.send()
35
+
36
+ expect(wrapper.getResponse()).toBeNull()
37
+ expect(wrapper.getError()).toBeInstanceOf(Error)
38
+ expect(wrapper.hasError()).toBe(true)
39
+ expect(wrapper.wasSent()).toBe(true)
40
+ })
41
+
42
+ it('passes abort signal to request', async () => {
43
+ const request = createRequest()
44
+ const wrapper = new BulkRequestWrapper(request as any)
45
+ const controller = new AbortController()
46
+
47
+ await wrapper.send(controller.signal)
48
+
49
+ expect(request.setAbortSignal).toHaveBeenCalledWith(controller.signal)
50
+ })
51
+ })
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { BasePaginator } from '../../../src/pagination/BasePaginator'
3
+ import { PaginationDataDto } from '../../../src/pagination/dtos/PaginationDataDto'
4
+ import type { BaseViewDriverContract } from '../../../src/pagination/contracts/BaseViewDriverContract'
5
+
6
+ class TestViewDriver implements BaseViewDriverContract<number[]> {
7
+ private data: number[] = []
8
+ private total = 0
9
+
10
+ setData(data: number[]): void {
11
+ this.data = data
12
+ }
13
+
14
+ getData(): number[] {
15
+ return this.data
16
+ }
17
+
18
+ setTotal(value: number): void {
19
+ this.total = value
20
+ }
21
+
22
+ getTotal(): number {
23
+ return this.total
24
+ }
25
+ }
26
+
27
+ class TestPaginator extends BasePaginator<number, TestViewDriver> {
28
+ protected override viewDriver = new TestViewDriver()
29
+
30
+ public exposePass(dto: PaginationDataDto<number[]>, options?: { flush?: boolean }) {
31
+ this.passDataToViewDriver(dto, options)
32
+ }
33
+
34
+ public exposeHandleStale() {
35
+ return this.handleStaleResponse()
36
+ }
37
+
38
+ public getViewDriver() {
39
+ return this.viewDriver
40
+ }
41
+ }
42
+
43
+ describe('BasePaginator', () => {
44
+ it('updates rows and returns updated count', () => {
45
+ const paginator = new TestPaginator(null)
46
+ paginator.getViewDriver().setData([1, 2, 3])
47
+
48
+ const updated = paginator.updateRows(
49
+ (row) => row % 2 === 0,
50
+ (row) => row * 10
51
+ )
52
+
53
+ expect(updated).toBe(1)
54
+ expect(paginator.getPageData()).toEqual([1, 20, 3])
55
+ })
56
+
57
+ it('removes rows and adjusts total', () => {
58
+ const paginator = new TestPaginator(null)
59
+ paginator.getViewDriver().setData([1, 2, 3])
60
+ paginator.getViewDriver().setTotal(3)
61
+
62
+ const removed = paginator.removeRows((row) => row > 1)
63
+
64
+ expect(removed).toBe(2)
65
+ expect(paginator.getPageData()).toEqual([1])
66
+ expect(paginator.getTotal()).toBe(1)
67
+ })
68
+
69
+ it('can remove rows without adjusting total', () => {
70
+ const paginator = new TestPaginator(null)
71
+ paginator.getViewDriver().setData([1, 2, 3])
72
+ paginator.getViewDriver().setTotal(3)
73
+
74
+ paginator.removeRows((row) => row === 1, { adjustTotal: false })
75
+
76
+ expect(paginator.getPageData()).toEqual([2, 3])
77
+ expect(paginator.getTotal()).toBe(3)
78
+ })
79
+
80
+ it('passes data to view driver and marks initialized', () => {
81
+ const paginator = new TestPaginator(null)
82
+
83
+ paginator.exposePass(new PaginationDataDto([4, 5], 2), { flush: true })
84
+
85
+ expect(paginator.getPageData()).toEqual([4, 5])
86
+ expect(paginator.getTotal()).toBe(2)
87
+ expect(paginator.isInitialized()).toBe(true)
88
+ })
89
+
90
+ it('handles stale response by returning current data/total', () => {
91
+ const paginator = new TestPaginator(null)
92
+ paginator.getViewDriver().setData([7])
93
+ paginator.getViewDriver().setTotal(9)
94
+
95
+ const dto = paginator.exposeHandleStale()
96
+
97
+ expect(dto.getData()).toEqual([7])
98
+ expect(dto.getTotal()).toBe(9)
99
+ })
100
+ })
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { InfiniteScroller } from '../../../src/pagination/InfiniteScroller'
3
+ import { PaginationDataDto } from '../../../src/pagination/dtos/PaginationDataDto'
4
+ import type { PaginationDataDriverContract } from '../../../src/pagination/contracts/PaginationDataDriverContract'
5
+ import type { ViewDriverFactoryContract } from '../../../src/pagination/contracts/ViewDriverFactoryContract'
6
+ import type { ViewDriverContract } from '../../../src/pagination/contracts/ViewDriverContract'
7
+
8
+ class StubViewDriver implements ViewDriverContract<number[]> {
9
+ private data: number[] = []
10
+ private total = 0
11
+ private currentPage: number
12
+ private pageSize: number
13
+
14
+ constructor(pageNumber: number, pageSize: number) {
15
+ this.currentPage = pageNumber
16
+ this.pageSize = pageSize
17
+ }
18
+
19
+ setData(data: number[]): void {
20
+ this.data = data
21
+ }
22
+
23
+ getData(): number[] {
24
+ return this.data
25
+ }
26
+
27
+ setTotal(value: number): void {
28
+ this.total = value
29
+ }
30
+
31
+ getTotal(): number {
32
+ return this.total
33
+ }
34
+
35
+ getCurrentPage(): number {
36
+ return this.currentPage
37
+ }
38
+
39
+ setPage(value: number): void {
40
+ this.currentPage = value
41
+ }
42
+
43
+ setPageSize(value: number): void {
44
+ this.pageSize = value
45
+ }
46
+
47
+ getPageSize(): number {
48
+ return this.pageSize
49
+ }
50
+
51
+ getLastPage(): number {
52
+ return Math.max(1, Math.ceil(this.total / this.pageSize))
53
+ }
54
+
55
+ getPages(): number[] {
56
+ return Array.from({ length: this.getLastPage() }, (_, i) => i + 1)
57
+ }
58
+ }
59
+
60
+ class StubViewDriverFactory implements ViewDriverFactoryContract {
61
+ public make<ResourceInterface>(pageNumber: number, pageSize: number): ViewDriverContract<ResourceInterface[]> {
62
+ return new StubViewDriver(pageNumber, pageSize) as ViewDriverContract<ResourceInterface[]>
63
+ }
64
+ }
65
+
66
+ describe('InfiniteScroller', () => {
67
+ it('concatenates data by default', async () => {
68
+ let call = 0
69
+ const dataDriver: PaginationDataDriverContract<number[]> = {
70
+ get: async () => {
71
+ call += 1
72
+ return new PaginationDataDto(call === 1 ? [1, 2] : [3], 3)
73
+ },
74
+ }
75
+
76
+ const paginator = new InfiniteScroller<number[]>(dataDriver, 1, 2, {
77
+ viewDriverFactory: new StubViewDriverFactory(),
78
+ })
79
+
80
+ await paginator.load()
81
+ await paginator.load()
82
+
83
+ expect(paginator.getPageData()).toEqual([1, 2, 3])
84
+ })
85
+
86
+ it('replaces data when replace is true', async () => {
87
+ const dataDriver: PaginationDataDriverContract<number[]> = {
88
+ get: async () => new PaginationDataDto([9], 1),
89
+ }
90
+
91
+ const paginator = new InfiniteScroller<number[]>(dataDriver, 1, 2, {
92
+ viewDriverFactory: new StubViewDriverFactory(),
93
+ })
94
+
95
+ ;(paginator as any).viewDriver.setData([1, 2])
96
+
97
+ await paginator.load(1, { replace: true })
98
+
99
+ expect(paginator.getPageData()).toEqual([9])
100
+ })
101
+ })
@@ -0,0 +1,133 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { PageAwarePaginator } from '../../../src/pagination/PageAwarePaginator'
3
+ import { PaginationDataDto } from '../../../src/pagination/dtos/PaginationDataDto'
4
+ import { StaleResponseException } from '../../../src/requests/exceptions/StaleResponseException'
5
+ import type { PaginationDataDriverContract } from '../../../src/pagination/contracts/PaginationDataDriverContract'
6
+ import type { ViewDriverContract } from '../../../src/pagination/contracts/ViewDriverContract'
7
+ import type { ViewDriverFactoryContract } from '../../../src/pagination/contracts/ViewDriverFactoryContract'
8
+
9
+ class StubViewDriver implements ViewDriverContract<number[]> {
10
+ private data: number[] = []
11
+ private total = 0
12
+ private currentPage: number
13
+ private pageSize: number
14
+
15
+ constructor(pageNumber: number, pageSize: number) {
16
+ this.currentPage = pageNumber
17
+ this.pageSize = pageSize
18
+ }
19
+
20
+ setData(data: number[]): void {
21
+ this.data = data
22
+ }
23
+
24
+ getData(): number[] {
25
+ return this.data
26
+ }
27
+
28
+ setTotal(value: number): void {
29
+ this.total = value
30
+ }
31
+
32
+ getTotal(): number {
33
+ return this.total
34
+ }
35
+
36
+ getCurrentPage(): number {
37
+ return this.currentPage
38
+ }
39
+
40
+ setPage(value: number): void {
41
+ this.currentPage = value
42
+ }
43
+
44
+ setPageSize(value: number): void {
45
+ this.pageSize = value
46
+ }
47
+
48
+ getPageSize(): number {
49
+ return this.pageSize
50
+ }
51
+
52
+ getLastPage(): number {
53
+ return Math.max(1, Math.ceil(this.total / this.pageSize))
54
+ }
55
+
56
+ getPages(): number[] {
57
+ return Array.from({ length: this.getLastPage() }, (_, i) => i + 1)
58
+ }
59
+ }
60
+
61
+ class StubViewDriverFactory implements ViewDriverFactoryContract {
62
+ public make<ResourceInterface>(pageNumber: number, pageSize: number): ViewDriverContract<ResourceInterface[]> {
63
+ return new StubViewDriver(pageNumber, pageSize) as ViewDriverContract<ResourceInterface[]>
64
+ }
65
+ }
66
+
67
+ describe('PageAwarePaginator', () => {
68
+ it('loads data and updates view driver', async () => {
69
+ const dataDriver: PaginationDataDriverContract<number[]> = {
70
+ get: async () => new PaginationDataDto([1, 2], 2),
71
+ }
72
+
73
+ const paginator = new PageAwarePaginator<number[]>(dataDriver, 1, 2, {
74
+ viewDriverFactory: new StubViewDriverFactory(),
75
+ })
76
+
77
+ const dto = await paginator.load(2)
78
+
79
+ expect(dto.getData()).toEqual([1, 2])
80
+ expect(paginator.getCurrentPage()).toBe(2)
81
+ expect(paginator.getPageData()).toEqual([1, 2])
82
+ expect(paginator.getTotal()).toBe(2)
83
+ })
84
+
85
+ it('computes item range and pages', () => {
86
+ const dataDriver: PaginationDataDriverContract<number[]> = {
87
+ get: async () => new PaginationDataDto([1], 10),
88
+ }
89
+
90
+ const paginator = new PageAwarePaginator<number[]>(dataDriver, 2, 5, {
91
+ viewDriverFactory: new StubViewDriverFactory(),
92
+ })
93
+
94
+ expect(paginator.getFromItemNumber()).toBe(6)
95
+ expect(paginator.getToItemNumber()).toBe(10)
96
+ paginator.getPageData()
97
+ })
98
+
99
+ it('resets page number when page size exceeds total', () => {
100
+ const dataDriver: PaginationDataDriverContract<number[]> = {
101
+ get: async () => new PaginationDataDto([1, 2], 3),
102
+ }
103
+
104
+ const paginator = new PageAwarePaginator<number[]>(dataDriver, 2, 2, {
105
+ viewDriverFactory: new StubViewDriverFactory(),
106
+ })
107
+
108
+ ;(paginator as any).viewDriver.setTotal(3)
109
+ paginator.setPageSize(2)
110
+
111
+ expect(paginator.getCurrentPage()).toBe(1)
112
+ })
113
+
114
+ it('returns stale data when StaleResponseException occurs', async () => {
115
+ const dataDriver: PaginationDataDriverContract<number[]> = {
116
+ get: async () => {
117
+ throw new StaleResponseException()
118
+ },
119
+ }
120
+
121
+ const paginator = new PageAwarePaginator<number[]>(dataDriver, 1, 2, {
122
+ viewDriverFactory: new StubViewDriverFactory(),
123
+ })
124
+
125
+ ;(paginator as any).viewDriver.setData([9])
126
+ ;(paginator as any).viewDriver.setTotal(9)
127
+
128
+ const dto = await paginator.load()
129
+
130
+ expect(dto.getData()).toEqual([9])
131
+ expect(dto.getTotal()).toBe(9)
132
+ })
133
+ })
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { StatePaginator } from '../../../src/pagination/StatePaginator'
3
+ import { StatePaginationDataDto } from '../../../src/pagination/dtos/StatePaginationDataDto'
4
+ import type { BaseViewDriverContract } from '../../../src/pagination/contracts/BaseViewDriverContract'
5
+ import type { BaseViewDriverFactoryContract } from '../../../src/pagination/contracts/BaseViewDriverFactoryContract'
6
+ import type { StatePaginationDataDriverContract } from '../../../src/pagination/contracts/StatePaginationDataDriverContract'
7
+
8
+ class StubViewDriver implements BaseViewDriverContract<number[]> {
9
+ private data: number[] = []
10
+ private total = 0
11
+
12
+ setData(data: number[]): void {
13
+ this.data = data
14
+ }
15
+
16
+ getData(): number[] {
17
+ return this.data
18
+ }
19
+
20
+ setTotal(value: number): void {
21
+ this.total = value
22
+ }
23
+
24
+ getTotal(): number {
25
+ return this.total
26
+ }
27
+ }
28
+
29
+ class StubViewDriverFactory implements BaseViewDriverFactoryContract {
30
+ public make<ResourceInterface>(): BaseViewDriverContract<ResourceInterface[]> {
31
+ return new StubViewDriver() as BaseViewDriverContract<ResourceInterface[]>
32
+ }
33
+ }
34
+
35
+ describe('StatePaginator', () => {
36
+ it('loads and appends data, tracking state', async () => {
37
+ const dataDriver: StatePaginationDataDriverContract<number[]> = {
38
+ get: async () => new StatePaginationDataDto([1, 2], 2, 'next'),
39
+ }
40
+
41
+ const paginator = new StatePaginator<number[]>(dataDriver, {
42
+ viewDriverFactory: new StubViewDriverFactory(),
43
+ })
44
+
45
+ const dto = await paginator.load()
46
+
47
+ expect(dto.getData()).toEqual([1, 2])
48
+ expect(paginator.getPageData()).toEqual([1, 2])
49
+ expect(paginator.getTotal()).toBe(2)
50
+ expect(paginator.getCurrentState()).toBe('next')
51
+ expect(paginator.hasNextPage()).toBe(true)
52
+ })
53
+
54
+ it('supports loadNext and replace option', async () => {
55
+ let call = 0
56
+ const dataDriver: StatePaginationDataDriverContract<number[]> = {
57
+ get: async (state) => {
58
+ call += 1
59
+ return new StatePaginationDataDto(call === 1 ? [1] : [2], 2, state ? null : 'next')
60
+ },
61
+ }
62
+
63
+ const paginator = new StatePaginator<number[]>(dataDriver, {
64
+ viewDriverFactory: new StubViewDriverFactory(),
65
+ })
66
+
67
+ await paginator.load()
68
+ await paginator.loadNext()
69
+
70
+ expect(paginator.getPageData()).toEqual([1, 2])
71
+ expect(paginator.hasNextPage()).toBe(false)
72
+
73
+ ;(paginator as any).passStateDataToViewDriver(new StatePaginationDataDto([9], 1, null), { replace: true })
74
+ expect(paginator.getPageData()).toEqual([9])
75
+ })
76
+ })