diff --git a/tests/playwright/tests/01-webhook.spec.ts b/tests/playwright/tests/01-webhook.spec.ts index 66b8b5cc8c..548c0f283c 100644 --- a/tests/playwright/tests/01-webhook.spec.ts +++ b/tests/playwright/tests/01-webhook.spec.ts @@ -3,6 +3,7 @@ import { DashboardPage } from '../pages/Dashboard'; import setup from '../setup'; import makeServer from '../setup/server'; import { WebhookFormPage } from '../pages/Dashboard/WebhookForm'; +import { isSubset } from '../utils'; const hookPath = 'http://localhost:9090/hook'; @@ -16,9 +17,11 @@ async function clearServerData({ request }) { await expect(await response.json()).toBe(0); } -async function verifyHookTrigger(count: number, value: string, request) { +async function verifyHookTrigger(count: number, value: string, request, expectedData?: any) { // Retry since there can be lag between the time the hook is triggered and the time the server receives the request let response; + + // retry since there can be lag between the time the hook is triggered and the time the server receives the request for (let i = 0; i < 20; i++) { response = await request.get(hookPath + '/count'); if ((await response.json()) === count) { @@ -30,17 +33,43 @@ async function verifyHookTrigger(count: number, value: string, request) { if (count) { let response; + + // retry since there can be lag between the time the hook is triggered and the time the server receives the request for (let i = 0; i < 20; i++) { response = await request.get(hookPath + '/last'); - if ((await response.json()).Title === value) { + const rspJson = await response.json(); + if (rspJson.data.rows[0].Title === value) { break; } await new Promise(resolve => setTimeout(resolve, 150)); } - await expect((await response.json()).Title).toBe(value); + const rspJson = await response.json(); + await expect(rspJson?.data?.rows[0]?.Title).toBe(value); + if (expectedData) { + await expect(isSubset(rspJson, expectedData)).toBe(true); + } } } +async function buildExpectedResponseData(type, value) { + const expectedData = { + type: 'records.after.insert', + data: { + table_name: 'Test', + view_name: 'Test', + rows: [ + { + Title: 'Poole', + }, + ], + }, + }; + + expectedData.type = type; + expectedData.data.rows[0].Title = value; + return expectedData; +} + test.describe.serial('Webhook', () => { // start a server locally for webhook tests @@ -58,7 +87,7 @@ test.describe.serial('Webhook', () => { }); test('CRUD', async ({ request, page }) => { - // todo: Waiting for the server to start + // Waiting for the server to start await page.waitForTimeout(1000); // close 'Team & Auth' tab @@ -66,6 +95,12 @@ test.describe.serial('Webhook', () => { await dashboard.closeTab({ title: 'Team & Auth' }); await dashboard.treeView.createTable({ title: 'Test' }); + // hook order + // hook-1: after insert + // - verify trigger after insert + // - verify no trigger after edit + // - verify no trigger after delete + // after insert hook await webhook.create({ title: 'hook-1', @@ -77,12 +112,24 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Poole', }); - await verifyHookTrigger(1, 'Poole', request); + await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole')); + + // trigger edit row & delete row + // verify that the hook is not triggered (count doesn't change in this case) await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); await verifyHookTrigger(1, 'Poole', request); await dashboard.grid.deleteRow(0); await verifyHookTrigger(1, 'Poole', request); + /////////////////////////////////////////////////////////////////////////// + + // hook order + // hook-1: after insert + // hook-2: after update + // - verify trigger after insert + // - verify trigger after edit + // - verify no trigger after delete + // after update hook await webhook.create({ title: 'hook-2', @@ -95,12 +142,22 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Poole', }); - await verifyHookTrigger(1, 'Poole', request); + await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole')); await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); - await verifyHookTrigger(2, 'Delaware', request); + await verifyHookTrigger(2, 'Delaware', request, buildExpectedResponseData('records.after.update', 'Delaware')); await dashboard.grid.deleteRow(0); await verifyHookTrigger(2, 'Delaware', request); + /////////////////////////////////////////////////////////////////////////// + + // hook order + // hook-1: after insert + // hook-2: after update + // hook-3: after delete + // - verify trigger after insert + // - verify trigger after edit + // - verify trigger after delete + // after delete hook await webhook.create({ title: 'hook-3', @@ -112,13 +169,24 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Poole', }); - await verifyHookTrigger(1, 'Poole', request); + await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole')); await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); - await verifyHookTrigger(2, 'Delaware', request); + await verifyHookTrigger(2, 'Delaware', request, buildExpectedResponseData('records.after.update', 'Delaware')); await dashboard.grid.deleteRow(0); - await verifyHookTrigger(3, 'Delaware', request); + await verifyHookTrigger(3, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware')); + + /////////////////////////////////////////////////////////////////////////// // modify webhook + + // hook order + // hook-1: after delete + // hook-2: after delete + // hook-3: after delete + // - verify no trigger after insert + // - verify no trigger after edit + // - verify trigger after delete + await webhook.open({ index: 0 }); await webhook.configureWebhook({ title: 'hook-1-modified', @@ -140,13 +208,28 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Poole', }); + + // for insert & edit, the hook should not be triggered (count doesn't change in this case) await verifyHookTrigger(0, 'Poole', request); await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); await verifyHookTrigger(0, 'Delaware', request); await dashboard.grid.deleteRow(0); - await verifyHookTrigger(3, 'Delaware', request); + + // for delete, the hook should be triggered (thrice in this case) + await verifyHookTrigger(3, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware')); + + /////////////////////////////////////////////////////////////////////////// // delete webhook + + // hook order + // hook-1: - + // hook-2: - + // hook-3: - + // - verify no trigger after insert + // - verify no trigger after edit + // - verify no trigger after delete + await webhook.delete({ index: 0 }); await webhook.delete({ index: 0 }); await webhook.delete({ index: 0 }); @@ -212,7 +295,18 @@ test.describe.serial('Webhook', () => { save: true, }); - // verify + /////////////////////////////////////////////////////////////////////////// + + // webhook with condition + + // hook order + // hook-1: after insert where Title is like 'Poole' + // hook-2: after update where Title is like 'Poole' + // hook-3: after delete where Title is like 'Poole' + // - verify trigger after insert gets triggered only when Title is like 'Poole' + // - verify trigger after edit gets triggered only when Title is like 'Poole' + // - verify trigger after delete gets triggered only when Title is like 'Poole' + await clearServerData({ request }); await dashboard.grid.addNewRow({ index: 0, @@ -224,15 +318,25 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Delaware', }); - await verifyHookTrigger(1, 'Poole', request); + await verifyHookTrigger(1, 'Poole', request, buildExpectedResponseData('records.after.insert', 'Poole')); await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); await dashboard.grid.editRow({ index: 1, value: 'Poole' }); - await verifyHookTrigger(2, 'Poole', request); + await verifyHookTrigger(2, 'Poole', request, buildExpectedResponseData('records.after.update', 'Poole')); await dashboard.grid.deleteRow(1); await dashboard.grid.deleteRow(0); - await verifyHookTrigger(3, 'Poole', request); + await verifyHookTrigger(3, 'Poole', request, buildExpectedResponseData('records.after.delete', 'Poole')); + + /////////////////////////////////////////////////////////////////////////// - // Delete condition + // webhook after conditions are removed + + // hook order + // hook-1: after insert + // hook-2: after update + // hook-3: after delete + // - verify trigger after insert gets triggered when Title is like 'Poole' or not + // - verify trigger after edit gets triggered when Title is like 'Poole' or not + // - verify trigger after delete gets triggered when Title is like 'Poole' or not await webhook.open({ index: 2 }); await webhook.deleteCondition({ save: true }); await webhook.open({ index: 1 }); @@ -251,12 +355,12 @@ test.describe.serial('Webhook', () => { columnHeader: 'Title', value: 'Delaware', }); - await verifyHookTrigger(2, 'Delaware', request); + await verifyHookTrigger(2, 'Delaware', request, buildExpectedResponseData('records.after.insert', 'Delaware')); await dashboard.grid.editRow({ index: 0, value: 'Delaware' }); await dashboard.grid.editRow({ index: 1, value: 'Poole' }); - await verifyHookTrigger(4, 'Poole', request); + await verifyHookTrigger(4, 'Poole', request, buildExpectedResponseData('records.after.update', 'Poole')); await dashboard.grid.deleteRow(1); await dashboard.grid.deleteRow(0); - await verifyHookTrigger(6, 'Delaware', request); + await verifyHookTrigger(6, 'Delaware', request, buildExpectedResponseData('records.after.delete', 'Delaware')); }); }); diff --git a/tests/playwright/tests/utils/general.ts b/tests/playwright/tests/utils/general.ts index 1571666b1a..2bb3bdf035 100644 --- a/tests/playwright/tests/utils/general.ts +++ b/tests/playwright/tests/utils/general.ts @@ -19,4 +19,23 @@ async function getTextExcludeIconText(selector) { return text.trim(); } -export { getTextExcludeIconText }; +function isSubset(obj, potentialSubset) { + for (const prop in potentialSubset) { + // eslint-disable-next-line no-prototype-builtins + if (potentialSubset.hasOwnProperty(prop)) { + const potentialValue = potentialSubset[prop]; + const objValue = obj[prop]; + if (typeof potentialValue === 'object' && typeof objValue === 'object') { + if (!isSubset(objValue, potentialValue)) { + return false; + } + // eslint-disable-next-line no-prototype-builtins + } else if (!obj.hasOwnProperty(prop) || objValue !== potentialValue) { + return false; + } + } + } + return true; +} + +export { getTextExcludeIconText, isSubset };