@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,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
|
+
});
|