@kronor/dtv 0.2.9

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 (97) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/copilot-instructions.md +64 -0
  3. package/.github/workflows/ci.yml +51 -0
  4. package/.husky/pre-commit +8 -0
  5. package/README.md +63 -0
  6. package/docs/api/README.md +32 -0
  7. package/docs/api/cell-renderers.md +121 -0
  8. package/docs/api/no-rows-component.md +71 -0
  9. package/docs/api/runtime.md +78 -0
  10. package/e2e/app.spec.ts +6 -0
  11. package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
  12. package/e2e/filter-sharing.spec.ts +113 -0
  13. package/e2e/filter-url-persistence.spec.ts +36 -0
  14. package/e2e/graphqlMock.ts +144 -0
  15. package/e2e/multi-field-filters.spec.ts +95 -0
  16. package/e2e/pagination.spec.ts +38 -0
  17. package/e2e/payment-request-email-filter.spec.ts +67 -0
  18. package/e2e/save-filter-splitbutton.spec.ts +68 -0
  19. package/e2e/simple-view-email-filter.spec.ts +67 -0
  20. package/e2e/simple-view-transforms.spec.ts +171 -0
  21. package/e2e/simple-view.spec.ts +104 -0
  22. package/e2e/transform-regression.spec.ts +108 -0
  23. package/eslint.config.js +30 -0
  24. package/index.html +17 -0
  25. package/jest.config.js +10 -0
  26. package/package.json +45 -0
  27. package/playwright.config.ts +54 -0
  28. package/public/vite.svg +1 -0
  29. package/src/App.externalRuntime.test.ts +190 -0
  30. package/src/App.tsx +540 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/components/AIAssistantForm.tsx +241 -0
  33. package/src/components/FilterForm.test.ts +82 -0
  34. package/src/components/FilterForm.tsx +375 -0
  35. package/src/components/PhoneNumberFilter.tsx +102 -0
  36. package/src/components/SavedFilterList.tsx +181 -0
  37. package/src/components/SpeechInput.tsx +67 -0
  38. package/src/components/Table.tsx +119 -0
  39. package/src/components/TablePagination.tsx +40 -0
  40. package/src/components/aiAssistant.test.ts +270 -0
  41. package/src/components/aiAssistant.ts +291 -0
  42. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
  43. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
  44. package/src/framework/cell-renderer-components/Link.tsx +28 -0
  45. package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
  46. package/src/framework/cell-renderer-components.test.ts +353 -0
  47. package/src/framework/column-definition.tsx +85 -0
  48. package/src/framework/currency.test.ts +46 -0
  49. package/src/framework/currency.ts +62 -0
  50. package/src/framework/data.staticConditions.test.ts +46 -0
  51. package/src/framework/data.test.ts +167 -0
  52. package/src/framework/data.ts +162 -0
  53. package/src/framework/filter-form-state.test.ts +189 -0
  54. package/src/framework/filter-form-state.ts +185 -0
  55. package/src/framework/filter-sharing.test.ts +135 -0
  56. package/src/framework/filter-sharing.ts +118 -0
  57. package/src/framework/filters.ts +194 -0
  58. package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
  59. package/src/framework/graphql.paginationKey.test.ts +29 -0
  60. package/src/framework/graphql.test.ts +286 -0
  61. package/src/framework/graphql.ts +462 -0
  62. package/src/framework/native-runtime/index.tsx +33 -0
  63. package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
  64. package/src/framework/runtime-reference.test.ts +172 -0
  65. package/src/framework/runtime.ts +15 -0
  66. package/src/framework/saved-filters.test.ts +422 -0
  67. package/src/framework/saved-filters.ts +293 -0
  68. package/src/framework/state.test.ts +86 -0
  69. package/src/framework/state.ts +148 -0
  70. package/src/framework/transform.test.ts +51 -0
  71. package/src/framework/view-parser-initialvalues.test.ts +228 -0
  72. package/src/framework/view-parser.ts +714 -0
  73. package/src/framework/view.test.ts +1805 -0
  74. package/src/framework/view.ts +38 -0
  75. package/src/index.css +6 -0
  76. package/src/main.tsx +99 -0
  77. package/src/views/index.ts +12 -0
  78. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
  79. package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
  80. package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
  81. package/src/views/payment-requests/index.ts +1 -0
  82. package/src/views/payment-requests/runtime.tsx +145 -0
  83. package/src/views/payment-requests/view.json +692 -0
  84. package/src/views/payment-requests-initial-values.test.ts +73 -0
  85. package/src/views/request-log/index.ts +2 -0
  86. package/src/views/request-log/runtime.tsx +47 -0
  87. package/src/views/request-log/view.json +123 -0
  88. package/src/views/simple-test-view/index.ts +3 -0
  89. package/src/views/simple-test-view/runtime.tsx +85 -0
  90. package/src/views/simple-test-view/view.json +191 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tailwind.config.js +7 -0
  93. package/tsconfig.app.json +26 -0
  94. package/tsconfig.jest.json +6 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +24 -0
  97. package/vite.config.ts +11 -0
@@ -0,0 +1,113 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Filter Sharing', () => {
5
+ test.beforeEach(async ({ page, context }) => {
6
+ // Grant clipboard permissions
7
+ await context.grantPermissions(['clipboard-read', 'clipboard-write']);
8
+
9
+ // Intercept the GraphQL request and mock the response
10
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
11
+ });
12
+
13
+ test('should show Share Filter button and handle sharing', async ({ page }) => {
14
+ // Navigate to the simple test view
15
+ await page.goto('/?test-view=simple-test-view');
16
+
17
+ // Wait for the table to be present and visible
18
+ const table = page.getByRole('table');
19
+ await expect(table).toBeVisible();
20
+
21
+ // Open the filter form
22
+ await page.getByText('Filters', { exact: true }).click();
23
+
24
+ // Verify Share Filter button exists in the filter form
25
+ const shareButton = page.getByRole('button', { name: 'Share Filter' });
26
+ await expect(shareButton).toBeVisible();
27
+
28
+ // If button is enabled, try sharing (some filter state may be present)
29
+ const isEnabled = await shareButton.isEnabled();
30
+ if (isEnabled) {
31
+ // Click the share button
32
+ await shareButton.click();
33
+
34
+ // Wait a bit for any potential toast to appear
35
+ await page.waitForTimeout(1000);
36
+
37
+ // Check if a toast appeared (it may or may not appear depending on success)
38
+ const toastMessages = page.locator('.p-toast .p-toast-message');
39
+ const toastCount = await toastMessages.count();
40
+
41
+ if (toastCount > 0) {
42
+ // If toast is present, verify it's about sharing
43
+ const toastText = await toastMessages.first().textContent();
44
+ expect(toastText).toMatch(/share|filter|copied|clipboard/i);
45
+ }
46
+
47
+ // The main test is that the button exists and can be clicked without error
48
+ }
49
+ });
50
+
51
+ test('should enable share button when filter is set', async ({ page }) => {
52
+ // Navigate to the simple test view
53
+ await page.goto('/?test-view=simple-test-view');
54
+
55
+ // Wait for the table to be present and visible
56
+ const table = page.getByRole('table');
57
+ await expect(table).toBeVisible();
58
+
59
+ // Open the filter form
60
+ await page.getByText('Filters', { exact: true }).click();
61
+
62
+ // Set a filter value using the pattern from working tests
63
+ const emailInput = page.getByText('Email', { exact: true }).locator('..').locator('~ div input');
64
+ await emailInput.fill('test@example.com');
65
+
66
+ // Apply the filter
67
+ await page.getByLabel('Apply filter').click();
68
+
69
+ // The filter form should still be open, verify Share Filter button is now enabled
70
+ const shareButton = page.getByRole('button', { name: 'Share Filter' });
71
+ await expect(shareButton).toBeEnabled();
72
+
73
+ // Click the share button
74
+ await shareButton.click();
75
+
76
+ // Wait a bit for any potential toast to appear
77
+ await page.waitForTimeout(1000);
78
+
79
+ // Check if a toast appeared
80
+ const toastMessages = page.locator('.p-toast .p-toast-message');
81
+ const toastCount = await toastMessages.count();
82
+
83
+ if (toastCount > 0) {
84
+ // If toast is present, verify it's about sharing
85
+ const toastText = await toastMessages.first().textContent();
86
+ expect(toastText).toMatch(/share|filter|copied|clipboard/i);
87
+ }
88
+
89
+ // The main test is that the button works and can be clicked
90
+ });
91
+
92
+ test('should handle invalid filter URL parameter', async ({ page }) => {
93
+ // Navigate with an invalid filter parameter
94
+ await page.goto('/?test-view=simple-test-view&dtv-filter-state=invalid-base64-data');
95
+
96
+ // Wait for the table to be present and visible (app should still load)
97
+ const table = page.getByRole('table');
98
+ await expect(table).toBeVisible();
99
+
100
+ // Give the app time to process the invalid filter and show toast
101
+ await page.waitForTimeout(1000);
102
+
103
+ // Check if any toast appears (might be warning about invalid filter)
104
+ const toast = page.locator('.p-toast');
105
+ const toastVisible = await toast.isVisible();
106
+
107
+ if (toastVisible) {
108
+ // If toast is present, verify it contains relevant text
109
+ const toastText = await toast.textContent();
110
+ expect(toastText).toMatch(/invalid|filter|error/i);
111
+ }
112
+ });
113
+ });
@@ -0,0 +1,36 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ function getParam(url: string, key: string) { return new URL(url).searchParams.get(key); }
5
+
6
+ // Base64 URL-safe pattern (rough)
7
+ const b64urlRe = /^[A-Za-z0-9_-]+$/;
8
+
9
+ test.describe('Filter state URL persistence flag', () => {
10
+ test('disabled: applying filter does not set dtv-filter-state', async ({ page }) => {
11
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
12
+ await page.goto('/?test-view=simple-test-view');
13
+ await page.getByText('Filters', { exact: true }).click();
14
+ const amountLabel = page.getByText('Amount', { exact: true });
15
+ const amountInput = amountLabel.locator('..').locator('~ div input');
16
+ await amountInput.fill('260');
17
+ await amountInput.press('Enter');
18
+ expect(getParam(page.url(), 'dtv-filter-state')).toBeNull();
19
+ });
20
+
21
+ test('enabled: applying filter sets and persists dtv-filter-state', async ({ page }) => {
22
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
23
+ await page.goto('/?test-view=simple-test-view&sync-filter-state-to-url=true');
24
+ await page.getByText('Filters', { exact: true }).click();
25
+ const amountLabel = page.getByText('Amount', { exact: true });
26
+ const amountInput = amountLabel.locator('..').locator('~ div input');
27
+ await amountInput.fill('260');
28
+ await amountInput.press('Enter');
29
+ const encoded = getParam(page.url(), 'dtv-filter-state');
30
+ expect(encoded).not.toBeNull();
31
+ expect(encoded).toMatch(b64urlRe);
32
+ await page.reload();
33
+ await expect(page.getByRole('table')).toBeVisible();
34
+ expect(getParam(page.url(), 'dtv-filter-state')).not.toBeNull();
35
+ });
36
+ });
@@ -0,0 +1,144 @@
1
+ // Shared GraphQL mock handler for e2e tests (pagination and simple view)
2
+ import { Route } from '@playwright/test';
3
+
4
+ // Fixed dataset: 30 items (1.5 pages at 20 per page)
5
+ const allRows = Array.from({ length: 30 }, (_, i) => ({
6
+ id: i + 1,
7
+ testField: `Test ${i + 1}`,
8
+ amount: (i + 1) * 10,
9
+ email: `user${i + 1}@example.com`,
10
+ phone: `+467000000${(i + 1).toString().padStart(2, '0')}`
11
+ }));
12
+
13
+ export async function mockPaginationGraphQL(route: Route) {
14
+ const request = route.request();
15
+ const postData = request.postDataJSON?.();
16
+ // Use paginationKey (id._gt or id._lt) to determine the start index, depending on orderBy
17
+ let startIdx = 0;
18
+ let pageSize = 20;
19
+ let orderKey = 'id';
20
+ let orderDir = 'asc';
21
+ if (postData && postData.variables && typeof postData.variables.limit === 'number') {
22
+ pageSize = postData.variables.limit;
23
+ }
24
+ if (postData && postData.variables && Array.isArray(postData.variables.orderBy) && postData.variables.orderBy.length > 0) {
25
+ const order = postData.variables.orderBy[0];
26
+ orderKey = Object.keys(order)[0];
27
+ orderDir = order[orderKey];
28
+ }
29
+ // Sort allRows according to orderBy
30
+ const sortedRows = allRows.slice();
31
+ sortedRows.sort((a, b) => {
32
+ if (a[orderKey] < b[orderKey]) return orderDir === 'ASC' ? -1 : 1;
33
+ if (a[orderKey] > b[orderKey]) return orderDir === 'ASC' ? 1 : -1;
34
+ return 0;
35
+ });
36
+ // Helper to recursively extract _gt/_lt for the pagination key from a condition tree
37
+ function findPaginationCursor(cond: any, key: string, orderDir: string): number | undefined {
38
+ if (!cond) return undefined;
39
+ if (cond._and && Array.isArray(cond._and)) {
40
+ for (const sub of cond._and) {
41
+ const found = findPaginationCursor(sub, key, orderDir);
42
+ if (found !== undefined) return found;
43
+ }
44
+ }
45
+ if (cond._or && Array.isArray(cond._or)) {
46
+ for (const sub of cond._or) {
47
+ const found = findPaginationCursor(sub, key, orderDir);
48
+ if (found !== undefined) return found;
49
+ }
50
+ }
51
+ if (cond[key]) {
52
+ if (orderDir === 'ASC' && cond[key]._gt !== undefined) return Number(cond[key]._gt);
53
+ if (orderDir === 'DESC' && cond[key]._lt !== undefined) return Number(cond[key]._lt);
54
+ }
55
+ return undefined;
56
+ }
57
+ // Apply additional filters (e.g. by amount, email)
58
+ function applyFilters(rows: typeof allRows, conditions: any): typeof allRows {
59
+ if (!conditions) return rows;
60
+
61
+ return rows.filter(row => evaluateCondition(row, conditions));
62
+ }
63
+
64
+ // Recursively evaluate a condition against a row
65
+ function evaluateCondition(row: any, condition: any): boolean {
66
+ // Handle logical operators
67
+ if (condition._and) {
68
+ return condition._and.every((subCondition: any) => evaluateCondition(row, subCondition));
69
+ }
70
+ if (condition._or) {
71
+ return condition._or.some((subCondition: any) => evaluateCondition(row, subCondition));
72
+ }
73
+ if (condition._not) {
74
+ return !evaluateCondition(row, condition._not);
75
+ }
76
+
77
+ // Handle field conditions
78
+ let pass = true;
79
+
80
+ // Check each field in the condition
81
+ for (const [fieldName, fieldCondition] of Object.entries(condition)) {
82
+ if (fieldName.startsWith('_')) continue; // Skip logical operators
83
+
84
+ const fieldValue = row[fieldName];
85
+ const ops = fieldCondition as any;
86
+
87
+ if (ops._eq !== undefined) {
88
+ if (fieldName === 'transformedField') {
89
+ // Special handling for transformedField - convert back to testField
90
+ const expectedValue = ops._eq.replace('prefix_', 'Test ');
91
+ pass = pass && row.testField === expectedValue;
92
+ } else {
93
+ pass = pass && fieldValue === ops._eq;
94
+ }
95
+ }
96
+ if (ops._neq !== undefined) pass = pass && fieldValue !== ops._neq;
97
+ if (ops._gt !== undefined) pass = pass && fieldValue > ops._gt;
98
+ if (ops._lt !== undefined) pass = pass && fieldValue < ops._lt;
99
+ if (ops._gte !== undefined) pass = pass && fieldValue >= ops._gte;
100
+ if (ops._lte !== undefined) pass = pass && fieldValue <= ops._lte;
101
+ if (ops._in !== undefined) pass = pass && Array.isArray(ops._in) && ops._in.includes(fieldValue);
102
+ if (ops._nin !== undefined) pass = pass && (!Array.isArray(ops._nin) || !ops._nin.includes(fieldValue));
103
+ if (ops._like !== undefined) {
104
+ // Convert SQL LIKE pattern to regex
105
+ const pattern = ops._like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.');
106
+ const regex = new RegExp(`^${pattern}$`);
107
+ pass = pass && regex.test(String(fieldValue));
108
+ }
109
+ if (ops._ilike !== undefined) {
110
+ // Convert SQL ILIKE pattern to case-insensitive regex
111
+ const pattern = ops._ilike.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.');
112
+ const regex = new RegExp(`^${pattern}$`, 'i');
113
+ pass = pass && regex.test(String(fieldValue));
114
+ }
115
+ if (ops._is_null !== undefined) pass = pass && ((fieldValue == null) === ops._is_null);
116
+ }
117
+
118
+ return pass;
119
+ }
120
+ // Pagination: use _gt for asc, _lt for desc, recursively
121
+ let cursorValue: number | undefined = undefined;
122
+ let filteredRows = sortedRows;
123
+ if (postData && postData.variables && postData.variables.conditions) {
124
+ // Apply filters first
125
+ filteredRows = applyFilters(sortedRows, postData.variables.conditions);
126
+ // Then pagination cursor
127
+ cursorValue = findPaginationCursor(postData.variables.conditions, orderKey, orderDir);
128
+ if (cursorValue !== undefined) {
129
+ const idx = filteredRows.findIndex(r => Number((r as any)[orderKey]) === cursorValue);
130
+ startIdx = idx >= 0 ? idx + 1 : 0;
131
+ }
132
+ }
133
+
134
+ const data = filteredRows.slice(startIdx, startIdx + pageSize);
135
+ await route.fulfill({
136
+ status: 200,
137
+ contentType: 'application/json',
138
+ body: JSON.stringify({
139
+ data: {
140
+ simpleTestDataCollection: data
141
+ }
142
+ })
143
+ });
144
+ }
@@ -0,0 +1,95 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Multi-field Filter Support', () => {
5
+ test('should handle OR multi-field filters correctly', async ({ page }) => {
6
+ // Intercept and mock GraphQL requests
7
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
8
+
9
+ // Navigate to the simple test view
10
+ await page.goto('/?test-view=simple-test-view');
11
+
12
+ // Wait for the table to be present
13
+ const table = page.getByRole('table');
14
+ await expect(table).toBeVisible();
15
+
16
+ // Show filters
17
+ await page.getByText('Filters', { exact: true }).click();
18
+
19
+ // Find the "Search Multiple Fields (OR)" filter
20
+ const multiFieldLabel = page.getByText('Search Multiple Fields (OR)', { exact: true });
21
+ await expect(multiFieldLabel).toBeVisible();
22
+
23
+ // Find the input for this filter (it should be in the same container)
24
+ const multiFieldInput = multiFieldLabel.locator('..').locator('~ div input');
25
+
26
+ // Enter a search term that should match either testField or email
27
+ await multiFieldInput.fill('%Test%');
28
+ await multiFieldInput.press('Enter');
29
+
30
+ // The table should still show data (our mock data has "Test" in testField)
31
+ await expect(table).toBeVisible();
32
+
33
+ // Check that we can see some test data - be more specific to avoid multiple matches
34
+ await expect(table.getByText('Test 30')).toBeVisible();
35
+ });
36
+
37
+ test('should handle AND multi-field filters correctly', async ({ page }) => {
38
+ // Intercept and mock GraphQL requests
39
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
40
+
41
+ // Navigate to the simple test view
42
+ await page.goto('/?test-view=simple-test-view');
43
+
44
+ // Wait for the table to be present
45
+ const table = page.getByRole('table');
46
+ await expect(table).toBeVisible();
47
+
48
+ // Show filters
49
+ await page.getByText('Filters', { exact: true }).click();
50
+
51
+ // Find the "Match Multiple Fields (AND)" filter
52
+ const andFieldLabel = page.getByText('Match Multiple Fields (AND)', { exact: true });
53
+ await expect(andFieldLabel).toBeVisible();
54
+
55
+ // Find the input for this filter
56
+ const andFieldInput = andFieldLabel.locator('..').locator('~ div input');
57
+
58
+ // Enter a value that would need to match both fields exactly
59
+ await andFieldInput.fill('exact_match');
60
+ await andFieldInput.press('Enter');
61
+
62
+ // The table should still be visible (even if no results, the structure remains)
63
+ await expect(table).toBeVisible();
64
+ });
65
+
66
+ test('should work with existing filters', async ({ page }) => {
67
+ // Intercept and mock GraphQL requests
68
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
69
+
70
+ // Navigate to the simple test view
71
+ await page.goto('/?test-view=simple-test-view');
72
+
73
+ // Wait for the table to be present
74
+ const table = page.getByRole('table');
75
+ await expect(table).toBeVisible();
76
+
77
+ // Show filters
78
+ await page.getByText('Filters', { exact: true }).click();
79
+
80
+ // Use both a regular filter and a multi-field filter
81
+ const emailLabel = page.getByText('Email', { exact: true });
82
+ const emailInput = emailLabel.locator('..').locator('~ div input');
83
+ await emailInput.fill('test@example.com');
84
+ await emailInput.press('Enter');
85
+
86
+ // Also use the multi-field OR filter
87
+ const multiFieldLabel = page.getByText('Search Multiple Fields (OR)', { exact: true });
88
+ const multiFieldInput = multiFieldLabel.locator('..').locator('~ div input');
89
+ await multiFieldInput.fill('%Test%');
90
+ await multiFieldInput.press('Enter');
91
+
92
+ // Both filters should work together
93
+ await expect(table).toBeVisible();
94
+ });
95
+ });
@@ -0,0 +1,38 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ const APP_URL = 'http://localhost:5173/?test-view=simple-test-view';
5
+ const nextButton = '[data-testid="pagination-next"]';
6
+ const prevButton = '[data-testid="pagination-prev"]';
7
+ const pageIndicator = '[data-testid="pagination-page"]';
8
+ const tableRows = 'table tbody tr';
9
+
10
+ test.describe('Simple View Pagination', () => {
11
+ test.beforeEach(async ({ page }) => {
12
+ await page.route('**/graphql', mockPaginationGraphQL);
13
+ await page.goto(APP_URL);
14
+ await page.waitForSelector(tableRows);
15
+ });
16
+
17
+ test('shows first page and disables previous button', async ({ page }) => {
18
+ await expect(page.locator(pageIndicator)).toHaveText('1-20');
19
+ await expect(page.locator(prevButton)).toBeDisabled();
20
+ const rowCount = await page.locator(tableRows).count();
21
+ expect(rowCount).toBe(20);
22
+ });
23
+
24
+ test('can go to next and previous page', async ({ page }) => {
25
+ await page.click(nextButton);
26
+ await expect(page.locator(pageIndicator)).toHaveText('21-30');
27
+ await expect(page.locator(prevButton)).toBeEnabled();
28
+ await page.click(prevButton);
29
+ await expect(page.locator(pageIndicator)).toHaveText('1-20');
30
+ });
31
+
32
+ test('next button disables on last page', async ({ page }) => {
33
+ await page.click(nextButton); // to 2nd page
34
+ await expect(page.locator(pageIndicator)).toHaveText('21-30');
35
+ await expect(page.locator(tableRows)).toHaveCount(10);
36
+ await expect(page.locator(nextButton)).toBeDisabled();
37
+ });
38
+ });
@@ -0,0 +1,67 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Simple View Email Filter', () => {
5
+ test('should set email filter when clicking on email', async ({ page }) => {
6
+ // Intercept the GraphQL request and mock the response
7
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
8
+
9
+ // Navigate to the simple test view
10
+ await page.goto('/?test-view=simple-test-view');
11
+
12
+ // Wait for the table to be present and visible
13
+ const table = page.getByRole('table');
14
+ await expect(table).toBeVisible();
15
+
16
+ // Verify that the table headers are rendered correctly
17
+ await expect(table.getByText('Test Column Header')).toBeVisible();
18
+ await expect(table.getByText('Email')).toBeVisible();
19
+
20
+ // Find the first email button in the Email column
21
+ const firstEmailButton = table.locator('td button').filter({ hasText: '@' }).first();
22
+ await expect(firstEmailButton).toBeVisible();
23
+
24
+ // Verify the button has the expected styling classes
25
+ await expect(firstEmailButton).toHaveClass(/text-blue-500/);
26
+ await expect(firstEmailButton).toHaveClass(/underline/);
27
+
28
+ // Get the email text before clicking
29
+ const emailText = await firstEmailButton.textContent();
30
+ expect(emailText).toMatch(/@/);
31
+ expect(emailText).toBeTruthy(); // Ensure it's not null
32
+
33
+ // Click the email button
34
+ await firstEmailButton.click();
35
+
36
+ // Show filters to see the filter form
37
+ await page.getByText('Filters', { exact: true }).click();
38
+
39
+ // Verify that the Email filter is now populated with the clicked email
40
+ const emailFilterInput = page.getByText('Email', { exact: true }).locator('..').locator('~ div input');
41
+ await expect(emailFilterInput).toHaveValue(emailText!);
42
+
43
+ // Verify that the table now shows only rows with that email (should be just 1 row)
44
+ const emailButtons = table.locator('td button').filter({ hasText: emailText! });
45
+ await expect(emailButtons).toHaveCount(1);
46
+ });
47
+
48
+ test('should show tooltip on email hover', async ({ page }) => {
49
+ // Intercept the GraphQL request and mock the response
50
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
51
+
52
+ // Navigate to the simple test view
53
+ await page.goto('/?test-view=simple-test-view');
54
+
55
+ // Wait for the table to be present and visible
56
+ const table = page.getByRole('table');
57
+ await expect(table).toBeVisible();
58
+
59
+ // Find the first email button
60
+ const emailButton = table.locator('td button').filter({ hasText: '@' }).first();
61
+ await expect(emailButton).toBeVisible();
62
+
63
+ // Check for title attribute (tooltip)
64
+ const title = await emailButton.getAttribute('title');
65
+ expect(title).toMatch(/Filter by email:/);
66
+ });
67
+ });
@@ -0,0 +1,68 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Save Filter SplitButton', () => {
5
+ test.beforeEach(async ({ page }) => {
6
+ // Set up GraphQL mock
7
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
8
+
9
+ // Navigate to the app
10
+ await page.goto('/?test-view=simple-test-view');
11
+ });
12
+
13
+ test('should render SplitButton for save filter', async ({ page }) => {
14
+ // Show filters
15
+ await page.getByText('Filters', { exact: true }).click();
16
+
17
+ // Look for the Save Filter SplitButton
18
+ const saveFilterButton = page.locator('.p-splitbutton').filter({ hasText: 'Save Filter' });
19
+ await expect(saveFilterButton).toBeVisible();
20
+
21
+ // Verify the main button text
22
+ const mainButton = saveFilterButton.locator('.p-splitbutton-defaultbutton');
23
+ await expect(mainButton).toContainText('Save Filter');
24
+
25
+ // Verify the dropdown exists
26
+ const dropdownButton = saveFilterButton.locator('.p-splitbutton-menubutton');
27
+ await expect(dropdownButton).toBeVisible();
28
+ });
29
+
30
+ test('should open dropdown menu when arrow is clicked', async ({ page }) => {
31
+ // Show filters
32
+ await page.getByText('Filters', { exact: true }).click();
33
+
34
+ // Find the SplitButton
35
+ const saveFilterButton = page.locator('.p-splitbutton').filter({ hasText: 'Save Filter' });
36
+ await expect(saveFilterButton).toBeVisible();
37
+
38
+ // Click the dropdown arrow
39
+ const dropdownButton = saveFilterButton.locator('.p-splitbutton-menubutton');
40
+ await dropdownButton.click();
41
+
42
+ // Should see a menu appear (even if empty)
43
+ const menu = page.locator('.p-menu', { hasText: /Update/ }).or(page.locator('.p-menu'));
44
+ await expect(menu.first()).toBeVisible();
45
+ });
46
+
47
+ test('main button click should trigger save dialog', async ({ page }) => {
48
+ // Show filters
49
+ await page.getByText('Filters', { exact: true }).click();
50
+
51
+ // Set up dialog handler to verify it's triggered
52
+ let dialogTriggered = false;
53
+ page.on('dialog', async dialog => {
54
+ expect(dialog.type()).toBe('prompt');
55
+ expect(dialog.message()).toBe('Enter a name for this filter:');
56
+ dialogTriggered = true;
57
+ await dialog.dismiss(); // Just dismiss to avoid actually saving
58
+ });
59
+
60
+ // Click the main save button
61
+ const saveFilterButton = page.locator('.p-splitbutton').filter({ hasText: 'Save Filter' });
62
+ await saveFilterButton.locator('.p-splitbutton-defaultbutton').click();
63
+
64
+ // Verify dialog was triggered
65
+ await page.waitForTimeout(500);
66
+ expect(dialogTriggered).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,67 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Simple View Email Filter', () => {
5
+ test('should set email filter when clicking on email', async ({ page }) => {
6
+ // Intercept the GraphQL request and mock the response
7
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
8
+
9
+ // Navigate to the simple test view
10
+ await page.goto('/?test-view=simple-test-view');
11
+
12
+ // Wait for the table to be present and visible
13
+ const table = page.getByRole('table');
14
+ await expect(table).toBeVisible();
15
+
16
+ // Verify that the table headers are rendered correctly
17
+ await expect(table.getByText('Test Column Header')).toBeVisible();
18
+ await expect(table.getByText('Email')).toBeVisible();
19
+
20
+ // Find the first email button in the Email column
21
+ const firstEmailButton = table.locator('td button').filter({ hasText: '@' }).first();
22
+ await expect(firstEmailButton).toBeVisible();
23
+
24
+ // Verify the button has the expected styling classes
25
+ await expect(firstEmailButton).toHaveClass(/text-blue-500/);
26
+ await expect(firstEmailButton).toHaveClass(/underline/);
27
+
28
+ // Get the email text before clicking
29
+ const emailText = await firstEmailButton.textContent();
30
+ expect(emailText).toMatch(/@/);
31
+ expect(emailText).toBeTruthy(); // Ensure it's not null
32
+
33
+ // Click the email button
34
+ await firstEmailButton.click();
35
+
36
+ // Show filters to see the filter form
37
+ await page.getByText('Filters', { exact: true }).click();
38
+
39
+ // Verify that the Email filter is now populated with the clicked email
40
+ const emailFilterInput = page.getByText('Email', { exact: true }).locator('..').locator('~ div input');
41
+ await expect(emailFilterInput).toHaveValue(emailText!);
42
+
43
+ // Verify that the table now shows only rows with that email (should be just 1 row)
44
+ const emailButtons = table.locator('td button').filter({ hasText: emailText! });
45
+ await expect(emailButtons).toHaveCount(1);
46
+ });
47
+
48
+ test('should show tooltip on email hover', async ({ page }) => {
49
+ // Intercept the GraphQL request and mock the response
50
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
51
+
52
+ // Navigate to the simple test view
53
+ await page.goto('/?test-view=simple-test-view');
54
+
55
+ // Wait for the table to be present and visible
56
+ const table = page.getByRole('table');
57
+ await expect(table).toBeVisible();
58
+
59
+ // Find the first email button
60
+ const emailButton = table.locator('td button').filter({ hasText: '@' }).first();
61
+ await expect(emailButton).toBeVisible();
62
+
63
+ // Check for title attribute (tooltip)
64
+ const title = await emailButton.getAttribute('title');
65
+ expect(title).toMatch(/Filter by email:/);
66
+ });
67
+ });