@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.
- package/.editorconfig +12 -0
- package/.github/copilot-instructions.md +64 -0
- package/.github/workflows/ci.yml +51 -0
- package/.husky/pre-commit +8 -0
- package/README.md +63 -0
- package/docs/api/README.md +32 -0
- package/docs/api/cell-renderers.md +121 -0
- package/docs/api/no-rows-component.md +71 -0
- package/docs/api/runtime.md +78 -0
- package/e2e/app.spec.ts +6 -0
- package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
- package/e2e/filter-sharing.spec.ts +113 -0
- package/e2e/filter-url-persistence.spec.ts +36 -0
- package/e2e/graphqlMock.ts +144 -0
- package/e2e/multi-field-filters.spec.ts +95 -0
- package/e2e/pagination.spec.ts +38 -0
- package/e2e/payment-request-email-filter.spec.ts +67 -0
- package/e2e/save-filter-splitbutton.spec.ts +68 -0
- package/e2e/simple-view-email-filter.spec.ts +67 -0
- package/e2e/simple-view-transforms.spec.ts +171 -0
- package/e2e/simple-view.spec.ts +104 -0
- package/e2e/transform-regression.spec.ts +108 -0
- package/eslint.config.js +30 -0
- package/index.html +17 -0
- package/jest.config.js +10 -0
- package/package.json +45 -0
- package/playwright.config.ts +54 -0
- package/public/vite.svg +1 -0
- package/src/App.externalRuntime.test.ts +190 -0
- package/src/App.tsx +540 -0
- package/src/assets/react.svg +1 -0
- package/src/components/AIAssistantForm.tsx +241 -0
- package/src/components/FilterForm.test.ts +82 -0
- package/src/components/FilterForm.tsx +375 -0
- package/src/components/PhoneNumberFilter.tsx +102 -0
- package/src/components/SavedFilterList.tsx +181 -0
- package/src/components/SpeechInput.tsx +67 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/TablePagination.tsx +40 -0
- package/src/components/aiAssistant.test.ts +270 -0
- package/src/components/aiAssistant.ts +291 -0
- package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
- package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
- package/src/framework/cell-renderer-components/Link.tsx +28 -0
- package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
- package/src/framework/cell-renderer-components.test.ts +353 -0
- package/src/framework/column-definition.tsx +85 -0
- package/src/framework/currency.test.ts +46 -0
- package/src/framework/currency.ts +62 -0
- package/src/framework/data.staticConditions.test.ts +46 -0
- package/src/framework/data.test.ts +167 -0
- package/src/framework/data.ts +162 -0
- package/src/framework/filter-form-state.test.ts +189 -0
- package/src/framework/filter-form-state.ts +185 -0
- package/src/framework/filter-sharing.test.ts +135 -0
- package/src/framework/filter-sharing.ts +118 -0
- package/src/framework/filters.ts +194 -0
- package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
- package/src/framework/graphql.paginationKey.test.ts +29 -0
- package/src/framework/graphql.test.ts +286 -0
- package/src/framework/graphql.ts +462 -0
- package/src/framework/native-runtime/index.tsx +33 -0
- package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
- package/src/framework/runtime-reference.test.ts +172 -0
- package/src/framework/runtime.ts +15 -0
- package/src/framework/saved-filters.test.ts +422 -0
- package/src/framework/saved-filters.ts +293 -0
- package/src/framework/state.test.ts +86 -0
- package/src/framework/state.ts +148 -0
- package/src/framework/transform.test.ts +51 -0
- package/src/framework/view-parser-initialvalues.test.ts +228 -0
- package/src/framework/view-parser.ts +714 -0
- package/src/framework/view.test.ts +1805 -0
- package/src/framework/view.ts +38 -0
- package/src/index.css +6 -0
- package/src/main.tsx +99 -0
- package/src/views/index.ts +12 -0
- package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
- package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
- package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
- package/src/views/payment-requests/index.ts +1 -0
- package/src/views/payment-requests/runtime.tsx +145 -0
- package/src/views/payment-requests/view.json +692 -0
- package/src/views/payment-requests-initial-values.test.ts +73 -0
- package/src/views/request-log/index.ts +2 -0
- package/src/views/request-log/runtime.tsx +47 -0
- package/src/views/request-log/view.json +123 -0
- package/src/views/simple-test-view/index.ts +3 -0
- package/src/views/simple-test-view/runtime.tsx +85 -0
- package/src/views/simple-test-view/view.json +191 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +7 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.jest.json +6 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { mockPaginationGraphQL } from './graphqlMock';
|
|
3
|
+
|
|
4
|
+
test.describe('Simple View Transform Functionality', () => {
|
|
5
|
+
|
|
6
|
+
test('should apply transform functions when filtering by amount', async ({ page }) => {
|
|
7
|
+
// Intercept the GraphQL request and mock the response
|
|
8
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
9
|
+
|
|
10
|
+
// Navigate to the page with the simple test view
|
|
11
|
+
await page.goto('/?test-view=simple-test-view');
|
|
12
|
+
|
|
13
|
+
// Wait for the table to be present and visible
|
|
14
|
+
const table = page.getByRole('table');
|
|
15
|
+
await expect(table).toBeVisible();
|
|
16
|
+
|
|
17
|
+
// Verify all rows are initially visible
|
|
18
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
19
|
+
await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
|
|
20
|
+
await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
|
|
21
|
+
await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
|
|
22
|
+
await expect(table.getByText('Test 26', { exact: true })).toBeVisible();
|
|
23
|
+
await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
|
|
24
|
+
await expect(table.getByText('$300', { exact: true })).toBeVisible(); // amount for Test 30
|
|
25
|
+
await expect(table.getByText('$290', { exact: true })).toBeVisible(); // amount for Test 29
|
|
26
|
+
await expect(table.getByText('$280', { exact: true })).toBeVisible(); // amount for Test 28
|
|
27
|
+
await expect(table.getByText('$270', { exact: true })).toBeVisible(); // amount for Test 27
|
|
28
|
+
await expect(table.getByText('$260', { exact: true })).toBeVisible(); // amount for Test 26
|
|
29
|
+
await expect(table.getByText('$250', { exact: true })).toBeVisible(); // amount for Test 25
|
|
30
|
+
|
|
31
|
+
// Show filters first
|
|
32
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
33
|
+
|
|
34
|
+
// Find the amount filter input
|
|
35
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
36
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
37
|
+
|
|
38
|
+
// Enter 255 in the input field
|
|
39
|
+
// With transform: toQuery adds 5, so 255 becomes 260 in the query
|
|
40
|
+
// This should show only rows with amount >= 260 (Test 26, 27, 28, 29, 30)
|
|
41
|
+
await amountInput.fill('255');
|
|
42
|
+
await amountInput.press('Enter');
|
|
43
|
+
|
|
44
|
+
// Wait for filtering to complete and verify results
|
|
45
|
+
// These should still be visible (amount >= 260)
|
|
46
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
47
|
+
await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
|
|
48
|
+
await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
|
|
49
|
+
await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
|
|
50
|
+
await expect(table.getByText('Test 26', { exact: true })).toBeVisible();
|
|
51
|
+
await expect(table.getByText('$300', { exact: true })).toBeVisible();
|
|
52
|
+
await expect(table.getByText('$290', { exact: true })).toBeVisible();
|
|
53
|
+
await expect(table.getByText('$280', { exact: true })).toBeVisible();
|
|
54
|
+
await expect(table.getByText('$270', { exact: true })).toBeVisible();
|
|
55
|
+
await expect(table.getByText('$260', { exact: true })).toBeVisible();
|
|
56
|
+
|
|
57
|
+
// These should not be visible (amount < 260)
|
|
58
|
+
await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
|
|
59
|
+
await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
|
|
60
|
+
await expect(table.getByText('Test 23', { exact: true })).not.toBeVisible();
|
|
61
|
+
await expect(table.getByText('$250', { exact: true })).not.toBeVisible();
|
|
62
|
+
await expect(table.getByText('$240', { exact: true })).not.toBeVisible();
|
|
63
|
+
await expect(table.getByText('$230', { exact: true })).not.toBeVisible();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should handle multiple filter value changes with transforms', async ({ page }) => {
|
|
67
|
+
// Intercept the GraphQL request and mock the response
|
|
68
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
69
|
+
|
|
70
|
+
// Navigate to the page with the simple test view
|
|
71
|
+
await page.goto('/?test-view=simple-test-view');
|
|
72
|
+
|
|
73
|
+
// Wait for the table to be present and visible
|
|
74
|
+
const table = page.getByRole('table');
|
|
75
|
+
await expect(table).toBeVisible();
|
|
76
|
+
|
|
77
|
+
// Show filters first
|
|
78
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
79
|
+
|
|
80
|
+
// Find the amount filter input
|
|
81
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
82
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
83
|
+
|
|
84
|
+
// Test 1: Enter 245 (transforms to 250), should show Test 25 and up
|
|
85
|
+
await amountInput.fill('245');
|
|
86
|
+
await amountInput.press('Enter');
|
|
87
|
+
|
|
88
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
89
|
+
await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
|
|
90
|
+
await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
|
|
91
|
+
|
|
92
|
+
// Test 2: Change to 265 (transforms to 270), should show Test 27 and up
|
|
93
|
+
await amountInput.clear();
|
|
94
|
+
await amountInput.fill('265');
|
|
95
|
+
await amountInput.press('Enter');
|
|
96
|
+
|
|
97
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
98
|
+
await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
|
|
99
|
+
await expect(table.getByText('Test 26', { exact: true })).not.toBeVisible();
|
|
100
|
+
await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
|
|
101
|
+
|
|
102
|
+
// Test 3: Clear filter should show all items again
|
|
103
|
+
await amountInput.clear();
|
|
104
|
+
await amountInput.press('Enter');
|
|
105
|
+
|
|
106
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
107
|
+
await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
|
|
108
|
+
await expect(table.getByText('Test 24', { exact: true })).toBeVisible();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should handle key-value transform objects', async ({ page }) => {
|
|
112
|
+
// Intercept the GraphQL request and mock the response
|
|
113
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
114
|
+
|
|
115
|
+
// Navigate to the page with the simple test view
|
|
116
|
+
await page.goto('/?test-view=simple-test-view');
|
|
117
|
+
|
|
118
|
+
// Wait for the table to be present and visible
|
|
119
|
+
const table = page.getByRole('table');
|
|
120
|
+
await expect(table).toBeVisible();
|
|
121
|
+
|
|
122
|
+
// Verify all rows are initially visible
|
|
123
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
124
|
+
await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
|
|
125
|
+
|
|
126
|
+
// Show filters first
|
|
127
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
128
|
+
|
|
129
|
+
// Find the extra filters panel
|
|
130
|
+
const extraFiltersPanel = page.locator('.p-panel-header', { hasText: 'Extra Filters' });
|
|
131
|
+
await expect(extraFiltersPanel).toBeVisible();
|
|
132
|
+
|
|
133
|
+
// Expand the extra filters panel if needed
|
|
134
|
+
if (await extraFiltersPanel.getAttribute('aria-expanded') !== 'true') {
|
|
135
|
+
await extraFiltersPanel.click();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find the key-value transform filter input
|
|
139
|
+
const keyValueLabel = page.getByText('Test Field (Key-Value Transform)', { exact: true });
|
|
140
|
+
await expect(keyValueLabel).toBeVisible();
|
|
141
|
+
|
|
142
|
+
const keyValueInput = keyValueLabel.locator('..').locator('~ div input');
|
|
143
|
+
await expect(keyValueInput).toBeVisible();
|
|
144
|
+
|
|
145
|
+
// Test 1: Enter a value that should be transformed
|
|
146
|
+
// The transform should add "prefix_" to the input and change the field to "transformedField"
|
|
147
|
+
await keyValueInput.fill('30');
|
|
148
|
+
await keyValueInput.press('Enter');
|
|
149
|
+
|
|
150
|
+
// Wait a bit for the filter to apply
|
|
151
|
+
await page.waitForTimeout(2000);
|
|
152
|
+
|
|
153
|
+
// Verify the correct row is shown (Test 30 should be visible since it matches the transformed query)
|
|
154
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
155
|
+
// Verify other rows are hidden
|
|
156
|
+
await expect(table.getByText('Test 29', { exact: true })).not.toBeVisible();
|
|
157
|
+
|
|
158
|
+
// Test 2: Clear the filter to verify all rows show again
|
|
159
|
+
await keyValueInput.clear();
|
|
160
|
+
await keyValueInput.press('Enter');
|
|
161
|
+
|
|
162
|
+
// Wait for the filter to clear
|
|
163
|
+
await page.waitForTimeout(1000);
|
|
164
|
+
|
|
165
|
+
// All rows should be visible again
|
|
166
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
167
|
+
await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
|
|
168
|
+
await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { mockPaginationGraphQL } from './graphqlMock';
|
|
3
|
+
|
|
4
|
+
test.describe('Simple View Rendering', () => {
|
|
5
|
+
|
|
6
|
+
test('should filter by phone using a custom filter component', async ({ page }) => {
|
|
7
|
+
// Intercept the GraphQL request and mock the response
|
|
8
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
9
|
+
|
|
10
|
+
// Navigate to the page with the simple test view
|
|
11
|
+
await page.goto('/?test-view=simple-test-view');
|
|
12
|
+
|
|
13
|
+
// Wait for the table to be present and visible
|
|
14
|
+
const table = page.getByRole('table');
|
|
15
|
+
await expect(table).toBeVisible();
|
|
16
|
+
|
|
17
|
+
// Show filters first
|
|
18
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
19
|
+
|
|
20
|
+
// Find the phone filter input (by placeholder or input type)
|
|
21
|
+
const phoneInput = page.locator('input[placeholder="Phone number"]');
|
|
22
|
+
|
|
23
|
+
const phoneNumber = '+46700000025';
|
|
24
|
+
await phoneInput.fill(phoneNumber);
|
|
25
|
+
|
|
26
|
+
// Submit the filter form (by aria-label)
|
|
27
|
+
await page.getByLabel('Apply filter').click();
|
|
28
|
+
|
|
29
|
+
// Wait for the table to update and check that results are filtered
|
|
30
|
+
await expect(table.getByText(phoneNumber)).toBeVisible();
|
|
31
|
+
});
|
|
32
|
+
test('should render a view with a single column header and data', async ({ page }) => {
|
|
33
|
+
// Intercept the GraphQL request and mock the response
|
|
34
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
35
|
+
|
|
36
|
+
// Navigate to the page with the simple test view
|
|
37
|
+
await page.goto('/?test-view=simple-test-view');
|
|
38
|
+
|
|
39
|
+
// Wait for the table to be present and visible
|
|
40
|
+
const table = page.getByRole('table');
|
|
41
|
+
await expect(table).toBeVisible();
|
|
42
|
+
|
|
43
|
+
// Get the expected header text from the simple test view JSON (first column name)
|
|
44
|
+
const expectedHeaderText = "Test Column Header";
|
|
45
|
+
|
|
46
|
+
// Locate the column header by its text content
|
|
47
|
+
const columnHeader = table.getByText(expectedHeaderText, { exact: true });
|
|
48
|
+
|
|
49
|
+
// Assert that the column header is visible
|
|
50
|
+
await expect(columnHeader).toBeVisible();
|
|
51
|
+
|
|
52
|
+
// Assert that all rows are visible before applying the filter
|
|
53
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
54
|
+
await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
|
|
55
|
+
await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
|
|
56
|
+
await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
|
|
57
|
+
await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
|
|
58
|
+
await expect(table.getByText('Test 24', { exact: true })).toBeVisible();
|
|
59
|
+
await expect(table.getByText('$300', { exact: true })).toBeVisible();
|
|
60
|
+
await expect(table.getByText('$290', { exact: true })).toBeVisible();
|
|
61
|
+
await expect(table.getByText('$280', { exact: true })).toBeVisible();
|
|
62
|
+
await expect(table.getByText('$270', { exact: true })).toBeVisible();
|
|
63
|
+
await expect(table.getByText('$250', { exact: true })).toBeVisible();
|
|
64
|
+
await expect(table.getByText('$240', { exact: true })).toBeVisible();
|
|
65
|
+
|
|
66
|
+
// Show filters first
|
|
67
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
68
|
+
|
|
69
|
+
// Use the filter to only show rows with amount >= 30
|
|
70
|
+
// Find the Amount label, then its parent, then the sibling div, then the input inside
|
|
71
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
72
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
73
|
+
await amountInput.fill('260');
|
|
74
|
+
await amountInput.press('Enter');
|
|
75
|
+
|
|
76
|
+
// Assert that only the filtered rows are visible
|
|
77
|
+
await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
|
|
78
|
+
await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
|
|
79
|
+
await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
|
|
80
|
+
await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should render filter group captions in the filter form', async ({ page }) => {
|
|
84
|
+
// Intercept the GraphQL request and mock the response
|
|
85
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
86
|
+
|
|
87
|
+
// Navigate to the page with the simple test view
|
|
88
|
+
await page.goto('/?test-view=simple-test-view');
|
|
89
|
+
|
|
90
|
+
// Show filters first
|
|
91
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
92
|
+
|
|
93
|
+
// Wait for the filter form to be present
|
|
94
|
+
const filterForm = page.locator('form');
|
|
95
|
+
await expect(filterForm).toBeVisible();
|
|
96
|
+
|
|
97
|
+
// Check for the presence of the new group label ("Extra Filters") as a Panel header
|
|
98
|
+
const extraFiltersPanel = page.locator('.p-panel-header', { hasText: 'Extra Filters' });
|
|
99
|
+
await expect(extraFiltersPanel).toBeVisible();
|
|
100
|
+
|
|
101
|
+
// Check that the filter label is present under the new group
|
|
102
|
+
await expect(page.getByText('Test Field', { exact: true })).toBeVisible();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { mockPaginationGraphQL } from './graphqlMock';
|
|
3
|
+
|
|
4
|
+
test.describe('Transform Regression Tests', () => {
|
|
5
|
+
|
|
6
|
+
test('should preserve display value after applying transform', async ({ page }) => {
|
|
7
|
+
// Intercept the GraphQL request and mock the response
|
|
8
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
9
|
+
|
|
10
|
+
// Navigate to the page with the simple test view
|
|
11
|
+
await page.goto('/?test-view=simple-test-view');
|
|
12
|
+
|
|
13
|
+
// Wait for the table to be present and visible
|
|
14
|
+
const table = page.getByRole('table');
|
|
15
|
+
await expect(table).toBeVisible();
|
|
16
|
+
|
|
17
|
+
// Show filters first
|
|
18
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
19
|
+
|
|
20
|
+
// Find the amount filter input
|
|
21
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
22
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
23
|
+
|
|
24
|
+
// Enter 255 in the input field
|
|
25
|
+
// With transform: toQuery adds 5, so 255 becomes 260 in the query
|
|
26
|
+
// Display should remain 255 (the user's input)
|
|
27
|
+
await amountInput.fill('255');
|
|
28
|
+
|
|
29
|
+
// Apply the filter
|
|
30
|
+
await page.getByRole('button', { name: 'Apply filter' }).click();
|
|
31
|
+
|
|
32
|
+
// Wait for the filter to be applied
|
|
33
|
+
await page.waitForTimeout(100);
|
|
34
|
+
|
|
35
|
+
// Check that the input still shows the original value (255) after applying the transform
|
|
36
|
+
// This is the regression test - it should show the original user input
|
|
37
|
+
await expect(amountInput).toHaveValue('255');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should correctly apply toQuery transforms', async ({ page }) => {
|
|
41
|
+
// Intercept the GraphQL request and mock the response
|
|
42
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
43
|
+
|
|
44
|
+
// Navigate to the page with the simple test view
|
|
45
|
+
await page.goto('/?test-view=simple-test-view');
|
|
46
|
+
|
|
47
|
+
// Wait for the table to be present and visible
|
|
48
|
+
const table = page.getByRole('table');
|
|
49
|
+
await expect(table).toBeVisible();
|
|
50
|
+
|
|
51
|
+
// Show filters first
|
|
52
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
53
|
+
|
|
54
|
+
// Find the amount filter input
|
|
55
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
56
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
57
|
+
|
|
58
|
+
// Test multiple values to ensure the transform roundtrip works correctly
|
|
59
|
+
const testValues = ['100', '255', '300'];
|
|
60
|
+
|
|
61
|
+
for (const testValue of testValues) {
|
|
62
|
+
// Clear and enter the test value
|
|
63
|
+
await amountInput.fill('');
|
|
64
|
+
await amountInput.fill(testValue);
|
|
65
|
+
|
|
66
|
+
// Apply the filter
|
|
67
|
+
await page.getByRole('button', { name: 'Apply filter' }).click();
|
|
68
|
+
|
|
69
|
+
// Wait for the filter to be applied
|
|
70
|
+
await page.waitForTimeout(100);
|
|
71
|
+
|
|
72
|
+
// Verify the input still shows the original value
|
|
73
|
+
await expect(amountInput).toHaveValue(testValue);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should handle empty values correctly with transforms', async ({ page }) => {
|
|
78
|
+
// Intercept the GraphQL request and mock the response
|
|
79
|
+
await page.route('**/v1/graphql', mockPaginationGraphQL);
|
|
80
|
+
|
|
81
|
+
// Navigate to the page with the simple test view
|
|
82
|
+
await page.goto('/?test-view=simple-test-view');
|
|
83
|
+
|
|
84
|
+
// Wait for the table to be present and visible
|
|
85
|
+
const table = page.getByRole('table');
|
|
86
|
+
await expect(table).toBeVisible();
|
|
87
|
+
|
|
88
|
+
// Show filters first
|
|
89
|
+
await page.getByText('Filters', { exact: true }).click();
|
|
90
|
+
|
|
91
|
+
// Find the amount filter input
|
|
92
|
+
const amountLabel = page.getByText('Amount', { exact: true });
|
|
93
|
+
const amountInput = amountLabel.locator('..').locator('~ div input');
|
|
94
|
+
|
|
95
|
+
// Enter a value, then clear it
|
|
96
|
+
await amountInput.fill('255');
|
|
97
|
+
await page.getByRole('button', { name: 'Apply filter' }).click();
|
|
98
|
+
await page.waitForTimeout(100);
|
|
99
|
+
|
|
100
|
+
// Clear the input
|
|
101
|
+
await amountInput.fill('');
|
|
102
|
+
await page.getByRole('button', { name: 'Apply filter' }).click();
|
|
103
|
+
await page.waitForTimeout(100);
|
|
104
|
+
|
|
105
|
+
// Verify the input remains empty
|
|
106
|
+
await expect(amountInput).toHaveValue('');
|
|
107
|
+
});
|
|
108
|
+
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{ ignores: ['dist'] },
|
|
9
|
+
{
|
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2020,
|
|
14
|
+
globals: globals.browser,
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
'react-hooks': reactHooks,
|
|
18
|
+
'react-refresh': reactRefresh,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
...reactHooks.configs.recommended.rules,
|
|
22
|
+
'react-refresh/only-export-components': [
|
|
23
|
+
'warn',
|
|
24
|
+
{ allowConstantExport: true },
|
|
25
|
+
],
|
|
26
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
27
|
+
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
)
|
package/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<link href="./src/index.css" rel="stylesheet">
|
|
9
|
+
<title>Filters Demo</title>
|
|
10
|
+
</head>
|
|
11
|
+
|
|
12
|
+
<body class="h-full w-full">
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="./src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
|
|
17
|
+
</html>
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kronor/dtv",
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "tsc -b && vite build",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"test": "playwright test",
|
|
11
|
+
"test-unit": "jest",
|
|
12
|
+
"prepare": "husky"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"graphql-request": "^7.1.2",
|
|
16
|
+
"primeflex": "^4.0.0",
|
|
17
|
+
"primeicons": "^7.0.0",
|
|
18
|
+
"primereact": "^10.9.5",
|
|
19
|
+
"react": "^19.0.0",
|
|
20
|
+
"react-dom": "^19.0.0",
|
|
21
|
+
"tailwindcss": "^4.1.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@eslint/js": "^9.21.0",
|
|
25
|
+
"@playwright/test": "^1.52.0",
|
|
26
|
+
"@tailwindcss/vite": "^4.1.1",
|
|
27
|
+
"@types/dom-speech-recognition": "^0.0.6",
|
|
28
|
+
"@types/jest": "^30.0.0",
|
|
29
|
+
"@types/node": "^22.15.21",
|
|
30
|
+
"@types/react": "^19.1.4",
|
|
31
|
+
"@types/react-dom": "^19.1.5",
|
|
32
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
33
|
+
"eslint": "^9.21.0",
|
|
34
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
35
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
|
36
|
+
"globals": "^15.15.0",
|
|
37
|
+
"husky": "^8.0.3",
|
|
38
|
+
"jest": "^30.0.3",
|
|
39
|
+
"jest-environment-jsdom": "^30.0.4",
|
|
40
|
+
"ts-jest": "^29.4.0",
|
|
41
|
+
"typescript": "~5.7.2",
|
|
42
|
+
"typescript-eslint": "^8.24.1",
|
|
43
|
+
"vite": "^6.2.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
5
|
+
*/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: './e2e',
|
|
8
|
+
/* Run tests in files in parallel */
|
|
9
|
+
fullyParallel: !process.env.CI,
|
|
10
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
11
|
+
forbidOnly: !!process.env.CI,
|
|
12
|
+
/* Retry on CI only */
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
/* Opt out of parallel tests on CI. */
|
|
15
|
+
workers: process.env.CI ? 1 : undefined,
|
|
16
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
17
|
+
reporter: process.env.CI ? [['github'], ['html']] : 'html',
|
|
18
|
+
/* Expect timeout for assertions */
|
|
19
|
+
expect: {
|
|
20
|
+
timeout: 10 * 1000, // 10 seconds for expect assertions
|
|
21
|
+
},
|
|
22
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
23
|
+
use: {
|
|
24
|
+
/* Base URL to use in actions like \`await page.goto('/')\`. */
|
|
25
|
+
baseURL: 'http://localhost:5173',
|
|
26
|
+
|
|
27
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
28
|
+
trace: 'on-first-retry',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/* Configure projects for major browsers */
|
|
32
|
+
projects: [
|
|
33
|
+
{
|
|
34
|
+
name: 'chromium',
|
|
35
|
+
use: { ...devices['Desktop Chrome'] },
|
|
36
|
+
},
|
|
37
|
+
// {
|
|
38
|
+
// name: 'firefox',
|
|
39
|
+
// use: { ...devices['Desktop Firefox'] },
|
|
40
|
+
// },
|
|
41
|
+
|
|
42
|
+
// {
|
|
43
|
+
// name: 'webkit',
|
|
44
|
+
// use: { ...devices['Desktop Safari'] },
|
|
45
|
+
// },
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
/* Run your local dev server before starting the tests */
|
|
49
|
+
webServer: {
|
|
50
|
+
command: 'npm run dev',
|
|
51
|
+
url: 'http://localhost:5173',
|
|
52
|
+
reuseExistingServer: !process.env.CI,
|
|
53
|
+
},
|
|
54
|
+
});
|
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|