diff --git a/packages/testing/playwright/README.md b/packages/testing/playwright/README.md index 6fb4e21bc2..ca21d14a08 100644 --- a/packages/testing/playwright/README.md +++ b/packages/testing/playwright/README.md @@ -101,21 +101,63 @@ test.describe('Proxy tests @capability:proxy', () => { The ProxyServer service supports recording HTTP requests for test mocking and replay. All proxied requests are automatically recorded by the mock server as described in the [Mock Server documentation](https://www.mock-server.com/proxy/record_and_replay.html). -```typescript -// Record all requests -await proxyServer.recordExpectations(); +#### Recording Expectations -// Record requests with matching criteria -await proxyServer.recordExpectations({ - method: 'POST', - path: '/api/workflows', - queryStringParameters: { - 'userId': ['123'] +```typescript +// Record all requests (the request is simplified/cleansed to method/path/body/query) +await proxyServer.recordExpectations('test-folder'); + +// Record with filtering and options +await proxyServer.recordExpectations('test-folder', { + host: 'googleapis.com', // Filter by host (partial match) + dedupe: true, // Remove duplicate requests + raw: false // Save cleaned requests (default) +}); + +// Record raw requests with all headers and metadata +await proxyServer.recordExpectations('test-folder', { + raw: true // Save complete original requests +}); + +// Record requests matching specific criteria +await proxyServer.recordExpectations('test-folder', { + pathOrRequestDefinition: { + method: 'POST', + path: '/api/workflows' } }); ``` -Recorded expectations are saved as JSON files in the `expectations/` directory with unique names based on the request details. When the ProxyServer fixture initializes, all saved expectations are automatically loaded and mocked for subsequent test runs. +#### Loading and Using Recorded Expectations + +Recorded expectations are saved as JSON files in the `expectations/` directory. To use them in tests, you must explicitly load them: + +```typescript +test('should use recorded expectations', async ({ proxyServer }) => { + // Load expectations from a specific folder + await proxyServer.loadExpectations('test-folder'); + + // Your test code here - requests will be mocked using loaded expectations +}); +``` + +#### Important: Cleanup Expectations + +**Remember to clean up expectations before or after test runs:** + +```typescript +test.beforeEach(async ({ proxyServer }) => { + // Clear any existing expectations before test + await proxyServer.clearAllExpectations(); +}); + +test.afterEach(async ({ proxyServer }) => { + // Or clear expectations after test + await proxyServer.clearAllExpectations(); +}); +``` + +This prevents expectations from one test affecting others and ensures test isolation. ## Writing Tests For guidelines on writing new tests, see [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/packages/testing/playwright/expectations/evaluations/1756994893546-oauth2.googleapis.com-POST-_token-58f6fdf2.json b/packages/testing/playwright/expectations/evaluations/1756994893546-oauth2.googleapis.com-POST-_token-58f6fdf2.json new file mode 100644 index 0000000000..f376b47701 --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893546-oauth2.googleapis.com-POST-_token-58f6fdf2.json @@ -0,0 +1,37 @@ +{ + "httpRequest": { + "method": "POST", + "path": "/token" + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["scaffolding on HTTPServer2"], + "Date": ["Thu, 04 Sep 2025 14:07:57 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "access_token": "mock_access_token_fjWwvFFZMgzvj8i08j8coeyRZz_p57s_Vqjk3v0kzrmOr2MJMUVYYlOII5Zq8BFU08drBzsz50Q-6f3S1MBt2dO3vYkzw1Ml7jnykQmUSz1wVce-M9zefdaFVeVuc1rjIviipVeO3ojF95FvghMMwnVvUF55ppzI3n_vRM1hM5sipbora7xs5Y0qaOQrX_-6SF_j99_bWI_Og-y5OdoapprB_aXdkaOn8U816ac5ldp8r7g2bdU0oqwukwxUvyyubc-h6Zc3WIdtXcn7BXmtUVWMMwZ4uBqtr6rUq2YkocVSV0sf2yRZjy1Wdp8aFo0wRk2i0V55mOcvVVskQ07w9", + "expires_in": 3599, + "token_type": "Bearer" + }, + "rawBytes": "eyJhY2Nlc3NfdG9rZW4iOiJ5YTI5LmMuYzBBU1JLMEdZcmU5R2VfNVYtRlJNS0UtRi0yT2wwNVZQdHppQVhGODh2aGFqbm9pQUc4Xzhib1hadmFVSnVvUUJjQnluUUl6ajdwckk1c3c3Y3RGMEpISWpGdWUyOGRXc25pZ00yeDVPOU40R3FRb05kc1VKUF9ibFJHN1VSNEtuMF9YcnV0Y3A3bEZ5cmZrT2R0Vk8yeU5FMzFuaUJFYmJYeDRhRTRlaGVNME1ISkJaNlV5aDFzQjV5d2Q1akl4NENzdTRxNHJEemI4SFlYZ3FqZUd2THJsc3AyRndNaFFGd1VkR2hnVWIxdGZVTE0wT3FaajdEV0I4aGozeTJ0Tk5KUmhCWXZvZ3lEaF9STmdaNi1Bb2xNM1B1eHRjam9mTG1WMHZZY2lsZVMwc3ByUEQ2eEVqWHpIMlNndld1NDM1VXBfdU96NVVEbVljbUgzZ3JjTndmdmpJTWNMUk5LSndoWHkySGx1SEpRZktUZThpUGEtN05uTTh4aEt4TEczODlDYjRyczFsNVJhZmJZUXAtaHRWV3lSVlctcTZKYWY3NlJuOEpZQnNKNXhkRlg3WXczMXNoWWJSbG84dzV6NGhleHJ3cGp3c29sV3N0NHdNb1UwdDRreU80c3h4ajBhQm5oWnprOHc3cE1CSjlTbDFrSWd2V3F3MnBGVVJZWFYzaFk0aGtWck9PSlVkVUoyZGVjSVZmMFgxV3NmNV9sdDJZN09lVzlidllYcVNZa2NZMTJfUzhRQi13NF8tT1YxNmhrX1diMkJKVV9hcWc4aVdrVk1wNDZJcUl1ZlEtMW9Tb3djY3dtTzFXV3NfVWc4d3I3Vm40OVVabVZGV0JRNDM2VWR6MHFVZWRhNzFTWTJyc25vVy1tMi1XeHhXSXdsNVltdmZqV3d2RkZaTWd6dmo4aTA4ajhjb2V5Ulp6X3A1N3NfVnFqazN2MGt6cm1PcjJNSk1VVllZbE9JSTVacThCRlUwOGRyQnpzejUwUS02ZjNTMU1CdDJkTzN2WWt6dzFNbDdqbnlrUW1VU3oxd1ZjZS1NOXplZmRhRlZlVnVjMXJqSXZpaXBWZU8zb2pGOTVGdmdoTU13blZ2VUY1NXBwekkzbl92Uk0xaE01c2lwYm9yYTd4czVZMHFhT1FyWF8tNlNGX2o5OV9iV0lfT2cteTVPZG9hcHByQl9hWGRrYU9uOFU4MTZhYzVsZHA4cjdnMmJkVTBvcXd1a3d4VXZ5eXViYy1oNlpjM1dJZHRYY243QlhtdFVWV01Nd1o0dUJxdHI2clVxMllrb2NWU1Ywc2YyeVJaankxV2RwOGFGbzB3UmsyaTBWNTVtT2N2VlZza1EwN3c5IiwiZXhwaXJlc19pbiI6MzU5OSwidG9rZW5fdHlwZSI6IkJlYXJlciJ9" + } + }, + "id": "1756994893546-oauth2.googleapis.com-POST-_token-58f6fdf2.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893547-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs-185c9dd7.json b/packages/testing/playwright/expectations/evaluations/1756994893547-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs-185c9dd7.json new file mode 100644 index 0000000000..7801bed78b --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893547-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs-185c9dd7.json @@ -0,0 +1,64 @@ +{ + "httpRequest": { + "method": "GET", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "queryStringParameters": { + "fields": ["sheets.properties"] + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:07:57 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "sheets": [ + { + "properties": { + "sheetId": 0, + "title": "Sheet1", + "index": 0, + "sheetType": "GRID", + "gridProperties": { + "rowCount": 2001, + "columnCount": 26 + } + } + }, + { + "properties": { + "sheetId": 1911651598, + "title": "Sheet2", + "index": 1, + "sheetType": "GRID", + "gridProperties": { + "rowCount": 1000, + "columnCount": 26 + } + } + } + ] + }, + "rawBytes": "ewogICJzaGVldHMiOiBbCiAgICB7CiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJzaGVldElkIjogMCwKICAgICAgICAidGl0bGUiOiAiU2hlZXQxIiwKICAgICAgICAiaW5kZXgiOiAwLAogICAgICAgICJzaGVldFR5cGUiOiAiR1JJRCIsCiAgICAgICAgImdyaWRQcm9wZXJ0aWVzIjogewogICAgICAgICAgInJvd0NvdW50IjogMjAwMSwKICAgICAgICAgICJjb2x1bW5Db3VudCI6IDI2CiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAic2hlZXRJZCI6IDE5MTE2NTE1OTgsCiAgICAgICAgInRpdGxlIjogIlNoZWV0MiIsCiAgICAgICAgImluZGV4IjogMSwKICAgICAgICAic2hlZXRUeXBlIjogIkdSSUQiLAogICAgICAgICJncmlkUHJvcGVydGllcyI6IHsKICAgICAgICAgICJyb3dDb3VudCI6IDEwMDAsCiAgICAgICAgICAiY29sdW1uQ291bnQiOiAyNgogICAgICAgIH0KICAgICAgfQogICAgfQogIF0KfQo=" + } + }, + "id": "1756994893547-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs-185c9dd7.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893548-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values__Sheet2_-7746917a.json b/packages/testing/playwright/expectations/evaluations/1756994893548-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values__Sheet2_-7746917a.json new file mode 100644 index 0000000000..740d4852dc --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893548-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values__Sheet2_-7746917a.json @@ -0,0 +1,66 @@ +{ + "httpRequest": { + "method": "GET", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/'Sheet2'", + "queryStringParameters": { + "valueRenderOption": ["UNFORMATTED_VALUE"], + "dateTimeRenderOption": ["FORMATTED_STRING"] + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:07:59 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "range": "Sheet2!A1:Z1000", + "majorDimension": "ROWS", + "values": [ + [ + "name", + "email", + "actual", + "op", + "output-row_number", + "output-itemIndex", + "output-runIndex", + "data", + "random-output" + ], + [ + "test", + "test", + 10, + "output", + 2, + 0, + 0, + "output-0.17321991314554896", + 0.47763178373020865 + ], + ["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477] + ] + }, + "rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTE6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAibmFtZSIsCiAgICAgICJlbWFpbCIsCiAgICAgICJhY3R1YWwiLAogICAgICAib3AiLAogICAgICAib3V0cHV0LXJvd19udW1iZXIiLAogICAgICAib3V0cHV0LWl0ZW1JbmRleCIsCiAgICAgICJvdXRwdXQtcnVuSW5kZXgiLAogICAgICAiZGF0YSIsCiAgICAgICJyYW5kb20tb3V0cHV0IgogICAgXSwKICAgIFsKICAgICAgInRlc3QiLAogICAgICAidGVzdCIsCiAgICAgIDEwLAogICAgICAib3V0cHV0IiwKICAgICAgMiwKICAgICAgMCwKICAgICAgMCwKICAgICAgIm91dHB1dC0wLjE3MzIxOTkxMzE0NTU0ODk2IiwKICAgICAgMC40Nzc2MzE3ODM3MzAyMDg2NQogICAgXSwKICAgIFsKICAgICAgImhlbGxvIiwKICAgICAgIndvbHJkIiwKICAgICAgMTA0LAogICAgICAiIiwKICAgICAgMywKICAgICAgMCwKICAgICAgMCwKICAgICAgIm91dHB1dC0wLjE0NjM3MDMwNjQ0OTgwMjUzIiwKICAgICAgMC4xMzAxNTA1ODA4ODUyNTQ3NwogICAgXQogIF0KfQo=" + } + }, + "id": "1756994893548-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values__Sheet2_-7746917a.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893549-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_2_1000-e7fa67bd.json b/packages/testing/playwright/expectations/evaluations/1756994893549-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_2_1000-e7fa67bd.json new file mode 100644 index 0000000000..f2362f1699 --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893549-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_2_1000-e7fa67bd.json @@ -0,0 +1,55 @@ +{ + "httpRequest": { + "method": "GET", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!2:1000", + "queryStringParameters": { + "valueRenderOption": ["UNFORMATTED_VALUE"], + "dateTimeRenderOption": ["FORMATTED_STRING"] + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:08:00 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "range": "Sheet2!A2:Z1000", + "majorDimension": "ROWS", + "values": [ + [ + "test", + "test", + 10, + "output", + 2, + 0, + 0, + "output-0.17321991314554896", + 0.47763178373020865 + ], + ["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477] + ] + }, + "rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTI6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAidGVzdCIsCiAgICAgICJ0ZXN0IiwKICAgICAgMTAsCiAgICAgICJvdXRwdXQiLAogICAgICAyLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTczMjE5OTEzMTQ1NTQ4OTYiLAogICAgICAwLjQ3NzYzMTc4MzczMDIwODY1CiAgICBdLAogICAgWwogICAgICAiaGVsbG8iLAogICAgICAid29scmQiLAogICAgICAxMDQsCiAgICAgICIiLAogICAgICAzLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTQ2MzcwMzA2NDQ5ODAyNTMiLAogICAgICAwLjEzMDE1MDU4MDg4NTI1NDc3CiAgICBdCiAgXQp9Cg==" + } + }, + "id": "1756994893549-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_2_1000-e7fa67bd.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-19f43fca.json b/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-19f43fca.json new file mode 100644 index 0000000000..3840795cd9 --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-19f43fca.json @@ -0,0 +1,63 @@ +{ + "httpRequest": { + "method": "POST", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values:batchUpdate", + "body": { + "contentType": "application/json", + "type": "JSON", + "json": { + "data": [ + { + "range": "Sheet2!C2", + "values": [[11]] + } + ], + "valueInputOption": "RAW" + }, + "rawBytes": "eyJkYXRhIjpbeyJyYW5nZSI6IlNoZWV0MiFDMiIsInZhbHVlcyI6W1sxMV1dfV0sInZhbHVlSW5wdXRPcHRpb24iOiJSQVcifQ==" + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:08:04 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "totalUpdatedRows": 1, + "totalUpdatedColumns": 1, + "totalUpdatedCells": 1, + "totalUpdatedSheets": 1, + "responses": [ + { + "spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "updatedRange": "Sheet2!C2", + "updatedRows": 1, + "updatedColumns": 1, + "updatedCells": 1 + } + ] + }, + "rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidG90YWxVcGRhdGVkUm93cyI6IDEsCiAgInRvdGFsVXBkYXRlZENvbHVtbnMiOiAxLAogICJ0b3RhbFVwZGF0ZWRDZWxscyI6IDEsCiAgInRvdGFsVXBkYXRlZFNoZWV0cyI6IDEsCiAgInJlc3BvbnNlcyI6IFsKICAgIHsKICAgICAgInNwcmVhZHNoZWV0SWQiOiAiMXpKdTh6THRGYzNyYlpQQVdxS0JoN1RJb3U1OVN0SnI3SExuMG5VeTBicXMiLAogICAgICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFDMiIsCiAgICAgICJ1cGRhdGVkUm93cyI6IDEsCiAgICAgICJ1cGRhdGVkQ29sdW1ucyI6IDEsCiAgICAgICJ1cGRhdGVkQ2VsbHMiOiAxCiAgICB9CiAgXQp9Cg==" + } + }, + "id": "1756994893550-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-19f43fca.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-PUT-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_1_1-c0a137d1.json b/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-PUT-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_1_1-c0a137d1.json new file mode 100644 index 0000000000..14e84c46d0 --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893550-sheets.googleapis.com-PUT-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_1_1-c0a137d1.json @@ -0,0 +1,61 @@ +{ + "httpRequest": { + "method": "PUT", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!1:1", + "body": { + "contentType": "application/json", + "type": "JSON", + "json": { + "range": "Sheet2!1:1", + "values": [ + [ + "name", + "email", + "actual", + "op", + "output-row_number", + "output-itemIndex", + "output-runIndex", + "data", + "random-output" + ] + ] + }, + "rawBytes": "eyJyYW5nZSI6IlNoZWV0MiExOjEiLCJ2YWx1ZXMiOltbIm5hbWUiLCJlbWFpbCIsImFjdHVhbCIsIm9wIiwib3V0cHV0LXJvd19udW1iZXIiLCJvdXRwdXQtaXRlbUluZGV4Iiwib3V0cHV0LXJ1bkluZGV4IiwiZGF0YSIsInJhbmRvbS1vdXRwdXQiXV19" + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:08:02 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "updatedRange": "Sheet2!A1:I1", + "updatedRows": 1, + "updatedColumns": 9, + "updatedCells": 9 + }, + "rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFBMTpJMSIsCiAgInVwZGF0ZWRSb3dzIjogMSwKICAidXBkYXRlZENvbHVtbnMiOiA5LAogICJ1cGRhdGVkQ2VsbHMiOiA5Cn0K" + } + }, + "id": "1756994893550-sheets.googleapis.com-PUT-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_1_1-c0a137d1.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893551-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_3_1000-1bdb2093.json b/packages/testing/playwright/expectations/evaluations/1756994893551-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_3_1000-1bdb2093.json new file mode 100644 index 0000000000..11906811da --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893551-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_3_1000-1bdb2093.json @@ -0,0 +1,44 @@ +{ + "httpRequest": { + "method": "GET", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!3:1000", + "queryStringParameters": { + "valueRenderOption": ["UNFORMATTED_VALUE"], + "dateTimeRenderOption": ["FORMATTED_STRING"] + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:08:07 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "range": "Sheet2!A3:Z1000", + "majorDimension": "ROWS", + "values": [ + ["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477] + ] + }, + "rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTM6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAiaGVsbG8iLAogICAgICAid29scmQiLAogICAgICAxMDQsCiAgICAgICIiLAogICAgICAzLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTQ2MzcwMzA2NDQ5ODAyNTMiLAogICAgICAwLjEzMDE1MDU4MDg4NTI1NDc3CiAgICBdCiAgXQp9Cg==" + } + }, + "id": "1756994893551-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_3_1000-1bdb2093.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/evaluations/1756994893552-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-65f181a6.json b/packages/testing/playwright/expectations/evaluations/1756994893552-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-65f181a6.json new file mode 100644 index 0000000000..867f421d25 --- /dev/null +++ b/packages/testing/playwright/expectations/evaluations/1756994893552-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-65f181a6.json @@ -0,0 +1,63 @@ +{ + "httpRequest": { + "method": "POST", + "path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values:batchUpdate", + "body": { + "contentType": "application/json", + "type": "JSON", + "json": { + "data": [ + { + "range": "Sheet2!C3", + "values": [[105]] + } + ], + "valueInputOption": "RAW" + }, + "rawBytes": "eyJkYXRhIjpbeyJyYW5nZSI6IlNoZWV0MiFDMyIsInZhbHVlcyI6W1sxMDVdXX1dLCJ2YWx1ZUlucHV0T3B0aW9uIjoiUkFXIn0=" + } + }, + "httpResponse": { + "statusCode": 200, + "reasonPhrase": "OK", + "headers": { + "x-l2-request-path": ["l2-managed-6"], + "X-XSS-Protection": ["0"], + "X-Frame-Options": ["SAMEORIGIN"], + "X-Content-Type-Options": ["nosniff"], + "Vary": ["Origin", "X-Origin", "Referer"], + "Server": ["ESF"], + "Date": ["Thu, 04 Sep 2025 14:08:11 GMT"], + "Content-Type": ["application/json; charset=UTF-8"], + "Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"] + }, + "body": { + "type": "JSON", + "json": { + "spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "totalUpdatedRows": 1, + "totalUpdatedColumns": 1, + "totalUpdatedCells": 1, + "totalUpdatedSheets": 1, + "responses": [ + { + "spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "updatedRange": "Sheet2!C3", + "updatedRows": 1, + "updatedColumns": 1, + "updatedCells": 1 + } + ] + }, + "rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidG90YWxVcGRhdGVkUm93cyI6IDEsCiAgInRvdGFsVXBkYXRlZENvbHVtbnMiOiAxLAogICJ0b3RhbFVwZGF0ZWRDZWxscyI6IDEsCiAgInRvdGFsVXBkYXRlZFNoZWV0cyI6IDEsCiAgInJlc3BvbnNlcyI6IFsKICAgIHsKICAgICAgInNwcmVhZHNoZWV0SWQiOiAiMXpKdTh6THRGYzNyYlpQQVdxS0JoN1RJb3U1OVN0SnI3SExuMG5VeTBicXMiLAogICAgICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFDMyIsCiAgICAgICJ1cGRhdGVkUm93cyI6IDEsCiAgICAgICJ1cGRhdGVkQ29sdW1ucyI6IDEsCiAgICAgICJ1cGRhdGVkQ2VsbHMiOiAxCiAgICB9CiAgXQp9Cg==" + } + }, + "id": "1756994893552-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-65f181a6.json", + "priority": 0, + "timeToLive": { + "unlimited": true + }, + "times": { + "unlimited": true + } +} diff --git a/packages/testing/playwright/expectations/GET-mock_endpoint.json b/packages/testing/playwright/expectations/proxy-server/GET-mock_endpoint.json similarity index 100% rename from packages/testing/playwright/expectations/GET-mock_endpoint.json rename to packages/testing/playwright/expectations/proxy-server/GET-mock_endpoint.json diff --git a/packages/testing/playwright/fixtures/base.ts b/packages/testing/playwright/fixtures/base.ts index 59488cf3b7..4aeb986078 100644 --- a/packages/testing/playwright/fixtures/base.ts +++ b/packages/testing/playwright/fixtures/base.ts @@ -182,7 +182,6 @@ export const test = base.extend({ const serverUrl = `http://${proxyServerContainer?.getHost()}:${proxyServerContainer?.getFirstMappedPort()}`; const proxyServer = new ProxyServer(serverUrl); - await proxyServer.loadExpectations(); await use(proxyServer); }, diff --git a/packages/testing/playwright/services/proxy-server.ts b/packages/testing/playwright/services/proxy-server.ts index c728b346c5..8944f08f91 100644 --- a/packages/testing/playwright/services/proxy-server.ts +++ b/packages/testing/playwright/services/proxy-server.ts @@ -4,11 +4,22 @@ import crypto from 'crypto'; import { promises as fs } from 'fs'; -import type { Expectation, HttpRequest } from 'mockserver-client'; +import type { Expectation, RequestDefinition } from 'mockserver-client'; import { mockServerClient as proxyServerClient } from 'mockserver-client'; -import type { MockServerClient, RequestResponse } from 'mockserver-client/mockServerClient'; +import type { HttpRequest, HttpResponse } from 'mockserver-client/mockServer'; +import type { + MockServerClient, + PathOrRequestDefinition, + RequestResponse, +} from 'mockserver-client/mockServerClient'; import { join } from 'path'; +export type RequestMade = { + httpRequest?: HttpRequest; + httpResponse?: HttpResponse; + timestamp?: string; +}; + export interface ProxyServerRequest { method: string; path: string; @@ -60,17 +71,18 @@ export class ProxyServer { } /** - * Load all expectations from the expectations directory and mock them + * Load all expectations from the specified subfolder and mock them */ - async loadExpectations(): Promise { + async loadExpectations(folderName: string): Promise { try { - const files = await fs.readdir(this.expectationsDir); + const targetDir = join(this.expectationsDir, folderName); + const files = await fs.readdir(targetDir); const jsonFiles = files.filter((file) => file.endsWith('.json')); const expectations: Expectation[] = []; for (const file of jsonFiles) { try { - const filePath = join(this.expectationsDir, file); + const filePath = join(targetDir, file); const fileContent = await fs.readFile(filePath, 'utf8'); const expectation = JSON.parse(fileContent); expectations.push(expectation); @@ -108,11 +120,12 @@ export class ProxyServer { /** * Verify that a request was received by ProxyServer */ - async verifyRequest(request: ProxyServerRequest, numberOfRequests: number): Promise { + async verifyRequest(request: RequestDefinition, numberOfRequests: number): Promise { try { - await this.client.verify(request, numberOfRequests); + await this.client.verify(request, numberOfRequests, numberOfRequests); return true; } catch (error) { + console.log('error', error); return false; } } @@ -120,16 +133,16 @@ export class ProxyServer { /** * Clear all expectations and logs from ProxyServer */ - async clearProxyServer(): Promise { + async clearAllExpectations(): Promise { try { - await this.client.clear(null, 'ALL'); + await this.client.clear('', 'ALL'); } catch (error) { throw new Error(`Failed to clear ProxyServer: ${JSON.stringify(error)}`); } } /** - * Create a simple GET request expectation with JSON response + * Create a request expectation with JSON response */ async createGetExpectation( path: string, @@ -161,40 +174,50 @@ export class ProxyServer { } /** - * Verify a GET request was made to ProxyServer + * Verify a request was made to ProxyServer */ - async wasGetRequestMade( - path: string, - queryParams?: Record, - numberOfRequests = 1, - ): Promise { - const queryStringParameters = queryParams - ? Object.entries(queryParams).reduce>((acc, [key, value]) => { - acc[key] = [value]; - return acc; - }, {}) - : undefined; + async wasRequestMade(request: RequestDefinition, numberOfRequests = 1): Promise { + return await this.verifyRequest(request, numberOfRequests); + } - return await this.verifyRequest( - { - method: 'GET', - path, - ...(queryStringParameters && { queryStringParameters }), - }, - numberOfRequests, - ); + async getAllRequestsMade(): Promise { + // @ts-expect-error mockserver types seem to be messed up + return await this.client.retrieveRecordedRequestsAndResponses(''); } /** * Retrieve recorded expectations and write to files + * + * @param folderName - Target folder name for saving expectation files + * @param options - Optional configuration + * @param options.pathOrRequestDefinition - Filter expectations by path or request definition + * @param options.host - Filter expectations by host name (partial match) + * @param options.dedupe - Remove duplicate expectations based on request + * @param options.raw - Save full original requests (true) or cleaned requests (false, default) + * - raw: false (default) - Saves only essential fields: method, path, queryStringParameters (GET), body (POST/PUT) + * - raw: true - Saves complete original request including all headers and metadata */ - async recordExpectations(request?: HttpRequest): Promise { + async recordExpectations( + folderName: string, + options?: { + pathOrRequestDefinition?: PathOrRequestDefinition; + host?: string; + dedupe?: boolean; + raw?: boolean; + }, + ): Promise { try { // Retrieve recorded expectations from the mock server - const recordedExpectations = await this.client.retrieveRecordedExpectations(request); + const recordedExpectations = await this.client.retrieveRecordedExpectations( + options?.pathOrRequestDefinition, + ); - // Ensure expectations directory exists - await fs.mkdir(this.expectationsDir, { recursive: true }); + // Create target directory path + const targetDir = join(this.expectationsDir, folderName); + + // Ensure target directory exists + await fs.mkdir(targetDir, { recursive: true }); + const seenRequests = new Set(); for (const expectation of recordedExpectations) { if ( @@ -208,25 +231,77 @@ export class ProxyServer { continue; } - // Generate unique filename based on request details - const requestData = { - method: expectation.httpRequest?.method, - path: expectation.httpRequest?.path, - queryStringParameters: expectation.httpRequest?.queryStringParameters, - headers: expectation.httpRequest?.headers, + // Extract host for filename and filtering + const headers = expectation.httpRequest.headers ?? {}; + const hostHeader = 'Host' in headers ? headers?.Host : undefined; + const hostName = Array.isArray(hostHeader) ? hostHeader[0] : (hostHeader ?? 'unknown-host'); + + if (options?.host && typeof hostName === 'string' && !hostName.includes(options.host)) { + continue; + } + + const method = expectation.httpRequest.method; + let requestForProcessing: Record | HttpRequest; + + if (options?.raw) { + // Use raw request without cleaning + requestForProcessing = expectation.httpRequest; + } else { + // Clean up the request data + const cleanedRequest: Record = { + method: expectation.httpRequest.method, + path: expectation.httpRequest.path, + }; + + // Include different fields based on method + if (method === 'GET') { + // For GET requests, include queryStringParameters if present + if (expectation.httpRequest.queryStringParameters) { + cleanedRequest.queryStringParameters = expectation.httpRequest.queryStringParameters; + } + } else if (method === 'POST' || method === 'PUT') { + // For POST/PUT requests, include body if present + if (expectation.httpRequest.body) { + cleanedRequest.body = expectation.httpRequest.body; + } + } + + requestForProcessing = cleanedRequest; + } + + // Dedupe expectations if requested + if (options?.dedupe) { + const dedupeKey = JSON.stringify(requestForProcessing); + + if (seenRequests.has(dedupeKey)) { + continue; + } + + seenRequests.add(dedupeKey); + } + + // Create expectation (cleaned or raw) + const processedExpectation: Expectation = { + ...expectation, + httpRequest: requestForProcessing, + times: { + unlimited: true, + }, }; + // Generate unique filename based on request details const hash = crypto .createHash('sha256') - .update(JSON.stringify(requestData)) + .update(JSON.stringify(requestForProcessing)) .digest('hex') .substring(0, 8); - const filename = `${expectation.httpRequest?.method?.toString()}-${expectation.httpRequest?.path?.replace(/[^a-zA-Z0-9]/g, '_')}-${hash}.json`; - const filePath = join(this.expectationsDir, filename); + const filename = `${Date.now()}-${hostName}-${method}-${expectation.httpRequest.path.replace(/[^a-zA-Z0-9]/g, '_')}-${hash}.json`; + processedExpectation.id = filename; + const filePath = join(targetDir, filename); // Write expectation to JSON file - await fs.writeFile(filePath, JSON.stringify(expectation, null, 2)); + await fs.writeFile(filePath, JSON.stringify(processedExpectation, null, 2)); } } catch (error) { throw new Error(`Failed to record expectations: ${JSON.stringify(error)}`); diff --git a/packages/testing/playwright/tests/ui/env-mock-server.spec.ts b/packages/testing/playwright/tests/ui/env-mock-server.spec.ts index 737c990109..8b1b5a6174 100644 --- a/packages/testing/playwright/tests/ui/env-mock-server.spec.ts +++ b/packages/testing/playwright/tests/ui/env-mock-server.spec.ts @@ -4,6 +4,10 @@ import { test, expect } from '../../fixtures/base'; // @capability:proxy tag ensures that test suite is only run when proxy is available test.describe('Proxy server @capability:proxy', () => { + test.beforeEach(async ({ proxyServer }) => { + await proxyServer.clearAllExpectations(); + }); + test('should verify ProxyServer container is running', async ({ proxyServer }) => { const mockResponse = await proxyServer.createGetExpectation('/health', { status: 'healthy', @@ -12,7 +16,7 @@ test.describe('Proxy server @capability:proxy', () => { assert(typeof mockResponse !== 'string'); expect(mockResponse.statusCode).toBe(201); - expect(await proxyServer.wasGetRequestMade('/health')).toBe(false); + expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/health' })).toBe(false); // Verify the mock endpoint works const healthResponse = await fetch(`${proxyServer.url}/health`); @@ -20,7 +24,7 @@ test.describe('Proxy server @capability:proxy', () => { const healthData = await healthResponse.json(); expect(healthData.status).toBe('healthy'); - expect(await proxyServer.wasGetRequestMade('/health')).toBe(true); + expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/health' })).toBe(true); }); test('should run a simple workflow calling http endpoint', async ({ n8n, proxyServer }) => { @@ -41,18 +45,22 @@ test.describe('Proxy server @capability:proxy', () => { // Verify the request was handled by mockserver expect( - await proxyServer.wasGetRequestMade('/data', { - test: '1', + await proxyServer.wasRequestMade({ + method: 'GET', + path: '/data', + queryStringParameters: { test: ['1'] }, }), ).toBe(true); }); test('should use stored expectations respond to api request', async ({ proxyServer }) => { + await proxyServer.loadExpectations('proxy-server'); + const response = await fetch(`${proxyServer.url}/mock-endpoint`); expect(response.ok).toBe(true); const data = await response.json(); expect(data.title).toBe('delectus aut autem'); - expect(await proxyServer.wasGetRequestMade('/mock-endpoint')).toBe(true); + expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/mock-endpoint' })).toBe(true); }); test('should run a simple workflow proxying HTTPS request', async ({ n8n }) => { diff --git a/packages/testing/playwright/tests/ui/evaluations.spec.ts b/packages/testing/playwright/tests/ui/evaluations.spec.ts new file mode 100644 index 0000000000..35a0bd484e --- /dev/null +++ b/packages/testing/playwright/tests/ui/evaluations.spec.ts @@ -0,0 +1,112 @@ +import { expect, test } from '../../fixtures/base'; + +test.describe('Evaluations @capability:proxy', () => { + test.beforeEach(async ({ n8n, proxyServer }) => { + await proxyServer.clearAllExpectations(); + + await n8n.goHome(); + await n8n.workflows.clickAddWorkflowButton(); + }); + + test('should load evaluations workflow and execute twice', async ({ n8n, api, proxyServer }) => { + await proxyServer.loadExpectations('evaluations'); + + await api.credentialApi.createCredentialFromDefinition({ + name: 'Test Google Sheets', + type: 'googleApi', + data: { + email: 'email@quickstart-1234.iam.gserviceaccount.com', + // mock private key + privateKey: `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDx1//AaoSkyHYl +npqS3+uaePYhJXKD/T1h6zGThAUooN7ZzWK46nNcU1vghQMTlPMHfUTbl4xzZxEL +OYjyTPOKpwJvhmy44MU+zTQYJuUaU4dQuOCnnC61CL91Xy+8GJd7PvdUeVRWENWu +zzO825Fxeiy2qnbrOJfhYh+f9znwWM2R8/V6LIp1HSWNBU0h/NCesmVhGwTP2H/P +wGgFPzl9+effW8TgmAukVuZoG+z8pOiqJnZLgTOO++PLyM6UJe560UnAbv0yP4y5 +lZ370XwOQ6gVIiB0+8Z2A3tJp6ackfoMfDYbuU+CAhFPqkdvXgbrYciUCr6fzINo +ImK6CcSDAgMBAAECggEAF0+XokdiI7QC11tzUMbuocQZDVbbs+c7/G08KRjnmmPv +NxU599L5baPHTlvj0QZhao5jjbsM2a7MkMVp8tkB/JJehLtzTVq1CHmlFNLi8Geu +ulQnq2A9jEuckMatBjdkmoeWNXlAbM9QmXn1ZbXQThzVpIHH1qJs2Veo7rVYy1bD ++hnzadyeXsHOC518wNAaF3b1UShybI3dlrHbXqqRmkOZP272IKfmvZ2KOcnFC+MT +cWLUGWBTq2YK+UJv09OXHEBnonrm18m2Sku+/PhFwjOiifIK/1MWILss60IB7dFm +7Fe7NAtYQMPZyDEqY5Xo+K4FwWYzfxfHPiJf7k0DqQKBgQD5Rz+HCZC8V5c1oK8/ +1hGthyh5JdXxW7C8D1WVuo7W2OHrOJSDXjGhsxMjnKYdq/1YybJl9XpQSvZeumto +YazNiJqAexIlpmEHLW5gDtX3xpM0dujuJudTHYfveugtR8i/EZpWpFKv45/6Rm33 +Yt2PaMjLuO7yW0buEjSQInHtHwKBgQD4XW44YujgF+xvMmx8+QyyNI2UNI1ZmnsU +VZLmDAn5+WDz5YtBXN9JGIXIk5279S7xzu9xyq7Ih6uedxE/hmzaHSZ1gl9Xasci +n86FGaGPm6RtEeZ8c68oqha7kddLoBwTPBoZq5NaCCaTh2TQkMPg+Ws3erM0pkyC +fqw1hzkYHQKBgQC2Iv3i3/VV+DXupCqIXRRrkx7abe/FO3aF4jppfXdSugNQR/YT +imZ/PIXWdmXVtk4VasIjx1oIgs1C57kE+qE1SAODrujSg5/Pi71jCFQEh54VLnEB +WYGZ9DDXpRkxxIqEOQtpFQWpqIrCZmWA5Ub3uttEJyrIADNyTfEEA3b0hwKBgHrn +STbQA2t5iz/PlQ4W9GhvRyxzAQu5PXTnj+UVSg6QkKDBE7NJsRjr8LA8FE9B2nRA +sg7+fJWxRYUKaNelvtIEoNZ/qIyKw3Zn3HvTHjcBj1GGDSfC24fk+5Dgb8j1t07x +a/0OAcIIzIYu9v2a1cPLyXnP10STksL0ymVGwEMlAoGBAK2dtYZllhooN/C4ssFW +nmfqICLWEc/UZSxmxau1rOz71GJiiHgXFmQgiZtpf3Qp3wKKtoFkf+sJ6zP2VX35 +2tJcTO9lKm6kNa3eaveE/NJrkH5a0IpxrvDT1TvmnapaNEKuGZJAX5BNaggDrfEJ +m82JpEptTfAxFHtd8+Sb0U2G +-----END PRIVATE KEY-----`, + }, + }); + + // Import the evaluations workflow + await n8n.canvas.importWorkflow('evaluations_loop.json', 'Evaluations'); + + // Open each node to ensure credentials are set + await n8n.canvas.openNode('When fetching a dataset row'); + await n8n.page.keyboard.press('Escape'); + + // Open each node to ensure credentials are set + await n8n.canvas.openNode('Set outputs'); + await n8n.page.keyboard.press('Escape'); + + // Execute workflow from canvas - first execution + await n8n.canvas.clickExecuteWorkflowButton(); + + // wait for first run to finish + await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 }); + + // wait for second run to finish + await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 }); + + // 💡 To update recordings, remove stored expectations, set real credentials above and rerecord here. + // await proxyServer.recordExpectations('evaluations', { host: 'google', dedupe: true }); + + const batchUpdateRequests = (await proxyServer.getAllRequestsMade()).filter((request) => { + const path = request.httpRequest?.path; + const method = request.httpRequest?.method; + + return method === 'POST' && typeof path === 'string' && path.endsWith('/values:batchUpdate'); + }); + + /** + * Original Table in Google Sheets + * The loop should execute twice over both rows here + * Incrementing each value by 1 (expression in Set Output node) + * + * name email actual + test test 10 + hello wolrd 104 + */ + + // Set output node was called twice in a loop, updating Google sheets output value + expect(batchUpdateRequests.length).toEqual(2); + expect((batchUpdateRequests[0]?.httpRequest?.body as { json: object })?.json).toEqual({ + data: [ + { + range: 'Sheet2!C2', + values: [[11]], + }, + ], + valueInputOption: 'RAW', + }); + expect((batchUpdateRequests[1]?.httpRequest?.body as { json: object })?.json).toEqual({ + data: [ + { + range: 'Sheet2!C3', + values: [[105]], + }, + ], + valueInputOption: 'RAW', + }); + }); +}); diff --git a/packages/testing/playwright/workflows/evaluations_loop.json b/packages/testing/playwright/workflows/evaluations_loop.json new file mode 100644 index 0000000000..8b6eb46cc9 --- /dev/null +++ b/packages/testing/playwright/workflows/evaluations_loop.json @@ -0,0 +1,160 @@ +{ + "nodes": [ + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "mode": "list", + "cachedResultName": "Evaluation test - mutasem", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1911651598, + "mode": "list", + "cachedResultName": "Sheet2", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit#gid=1911651598" + } + }, + "type": "n8n-nodes-base.evaluationTrigger", + "typeVersion": 4.6, + "position": [0, 0], + "id": "2353d300-628d-4e9f-86ad-89b3b78bf02f", + "name": "When fetching a dataset row", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "DQuAchCa7lPMNsOG", + "name": "Google Sheets account" + } + } + }, + { + "parameters": { + "documentId": { + "__rl": true, + "value": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs", + "mode": "list", + "cachedResultName": "Evaluation test - mutasem", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit?usp=drivesdk" + }, + "sheetName": { + "__rl": true, + "value": 1911651598, + "mode": "list", + "cachedResultName": "Sheet2", + "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit#gid=1911651598" + }, + "outputs": { + "values": [ + { + "outputName": "actual", + "outputValue": "={{ parseInt($json.actual) + 1 }}" + } + ] + } + }, + "type": "n8n-nodes-base.evaluation", + "typeVersion": 4.7, + "position": [640, 0], + "id": "47bf60d4-1c60-4dcc-a9d7-af0c02c84db3", + "name": "Set outputs", + "credentials": { + "googleSheetsOAuth2Api": { + "id": "DQuAchCa7lPMNsOG", + "name": "Google Sheets account" + } + } + }, + { + "parameters": { + "amount": 1 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [208, 0], + "id": "2b5f3437-19bc-4d2e-ac60-52bfbb0fec1b", + "name": "Wait", + "webhookId": "26826666-e8e8-492e-9619-49e604fc90ee" + }, + { + "parameters": { + "operation": "setInputs", + "inputs": { + "values": [ + { + "inputName": "input", + "inputValue": "test" + } + ] + } + }, + "type": "n8n-nodes-base.evaluation", + "typeVersion": 4.7, + "position": [864, 0], + "id": "e027893e-4150-42dc-8b1e-8c24f5060cd1", + "name": "set inptus" + }, + { + "parameters": { + "operation": "checkIfEvaluating" + }, + "type": "n8n-nodes-base.evaluation", + "typeVersion": 4.7, + "position": [416, 0], + "id": "7cc3b2fb-985b-437d-a0c7-0375f3717ee1", + "name": "Evaluation" + } + ], + "connections": { + "When fetching a dataset row": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set outputs": { + "main": [ + [ + { + "node": "set inptus", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Evaluation", + "type": "main", + "index": 0 + } + ] + ] + }, + "Evaluation": { + "main": [ + [ + { + "node": "Set outputs", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "f0e9801eba0feea6a9ddf9beeabe34b0843eae42a1dbc62eaadd68e8f576be64" + } +}