From 55d292956643ebc00145fe30e0d43b9a6d8bd09e Mon Sep 17 00:00:00 2001 From: Makito Date: Fri, 19 Apr 2024 11:57:51 +0800 Subject: [PATCH] feat(*): expressions routes support [KM-20] (#1329) * feat(*): expressions routes support * feat(entities-routes): i18n texts and tooltips * feat(entities-routes): reduce dist size upper limit * Revert "feat(entities-routes): reduce dist size upper limit" This reverts commit 1dd5cd834b42b0d5ed3e1d1fed7457524ce75a06. * feat(entities-routes): use design tokens in style * feat(*): tab hash, expr column, tests, slots, and slide-outs Squashed from: fix(entities-routes): tabs hash feat(*): add prop to control expression column fix(entities-routes): fix i18n and tab hashes fix(entities-routes): tests feat(entities-routes): slots feat(entities-routes): expose more to the slot feat(entities-routes): add setter func to slot feat(entities-routes): adjust shape fix(entities-routes): fix config slide out * fix(*): remove umd, add tooltips for expr, fix tooltip * fix(entities-routes): types --- packages/core/expressions/package.json | 6 +- .../src/components/ExpressionsEditor.vue | 10 +- packages/core/expressions/src/index.ts | 2 +- packages/core/expressions/src/monaco.ts | 5 +- packages/core/expressions/src/schema.ts | 27 +- packages/core/expressions/vite.config.ts | 3 +- .../entities-routes/docs/route-form.md | 56 +- .../entities-routes/docs/route-list.md | 14 +- .../entities-routes/fixtures/mockData.ts | 72 +- .../entities/entities-routes/package.json | 11 +- .../sandbox/pages/RouteFormPage.vue | 64 +- .../sandbox/pages/RouteListPage.vue | 57 +- .../src/components/RouteForm.cy.ts | 2157 +++++++++++------ .../src/components/RouteForm.vue | 1198 +++++---- .../src/components/RouteFormConfigTabs.vue | 122 + .../components/RouteFormExpressionsEditor.vue | 24 + .../RouteFormExpressionsEditorLoader.vue | 71 + .../src/components/RouteList.cy.ts | 108 +- .../src/components/RouteList.vue | 49 +- .../entities-routes/src/locales/en.json | 25 +- .../entities-routes/src/types/route-form.ts | 96 +- .../entities-routes/src/types/route-list.ts | 2 - .../entities/entities-routes/tsconfig.json | 3 +- .../entities/entities-routes/vite.config.ts | 12 +- .../entity-base-table/EntityBaseTable.vue | 3 +- .../entity-base-table/EntityBaseTableCell.vue | 29 +- .../components/entity-filter/EntityFilter.vue | 2 +- .../entity-form-section/EntityFormSection.vue | 1 + .../entities-shared/src/types/base.ts | 5 + pnpm-lock.yaml | 50 +- 30 files changed, 2968 insertions(+), 1316 deletions(-) create mode 100644 packages/entities/entities-routes/src/components/RouteFormConfigTabs.vue create mode 100644 packages/entities/entities-routes/src/components/RouteFormExpressionsEditor.vue create mode 100644 packages/entities/entities-routes/src/components/RouteFormExpressionsEditorLoader.vue diff --git a/packages/core/expressions/package.json b/packages/core/expressions/package.json index 0fd0f53e97..2a07c8f5d9 100644 --- a/packages/core/expressions/package.json +++ b/packages/core/expressions/package.json @@ -2,7 +2,6 @@ "name": "@kong-ui-public/expressions", "version": "0.1.7", "type": "module", - "main": "./dist/expressions.umd.js", "module": "./dist/expressions.es.js", "types": "dist/types/index.d.ts", "files": [ @@ -11,7 +10,6 @@ "exports": { ".": { "import": "./dist/expressions.es.js", - "require": "./dist/expressions.umd.js", "types": "./dist/types/index.d.ts" }, "./package.json": "./package.json", @@ -42,7 +40,7 @@ "@kong/atc-router": "1.6.0-rc.1", "@kong/design-tokens": "1.12.11", "@kong/kongponents": "9.0.0-alpha.146", - "monaco-editor": "0.47.0", + "monaco-editor": "0.21.3", "vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", @@ -68,7 +66,7 @@ "peerDependencies": { "@kong/atc-router": "1.6.0-rc.1", "@kong/kongponents": "9.0.0-alpha.105", - "monaco-editor": "0.47.0", + "monaco-editor": "0.21.3", "vue": "^3.4.21" }, "dependencies": { diff --git a/packages/core/expressions/src/components/ExpressionsEditor.vue b/packages/core/expressions/src/components/ExpressionsEditor.vue index e6f344a0c7..5ffb5d0dfd 100644 --- a/packages/core/expressions/src/components/ExpressionsEditor.vue +++ b/packages/core/expressions/src/components/ExpressionsEditor.vue @@ -7,23 +7,23 @@ + + diff --git a/packages/entities/entities-routes/sandbox/pages/RouteListPage.vue b/packages/entities/entities-routes/sandbox/pages/RouteListPage.vue index f04ab1a847..007905b028 100644 --- a/packages/entities/entities-routes/sandbox/pages/RouteListPage.vue +++ b/packages/entities/entities-routes/sandbox/pages/RouteListPage.vue @@ -2,6 +2,23 @@ + + + +
+ +
+
+
+

Konnect API

+ + diff --git a/packages/entities/entities-routes/src/components/RouteForm.cy.ts b/packages/entities/entities-routes/src/components/RouteForm.cy.ts index 764365e0c0..b4756ca8b2 100644 --- a/packages/entities/entities-routes/src/components/RouteForm.cy.ts +++ b/packages/entities/entities-routes/src/components/RouteForm.cy.ts @@ -1,7 +1,8 @@ -import type { KonnectRouteFormConfig, KongManagerRouteFormConfig } from '../types' +import type { KonnectRouteFormConfig, KongManagerRouteFormConfig, RouteFlavors } from '../types' import RouteForm from './RouteForm.vue' -import { route, services } from '../../fixtures/mockData' +import { route, routeExpressions, services } from '../../fixtures/mockData' import { EntityBaseForm } from '@kong-ui-public/entities-shared' +import type { RouteHandler } from 'cypress/types/net-stubbing' const cancelRoute = { name: 'route-list' } @@ -19,6 +20,14 @@ const baseConfigKM: KongManagerRouteFormConfig = { cancelRoute, } +const TRADITIONAL_ONLY: RouteFlavors = { traditional: true, expressions: false } +const EXPRESSIONS_ONLY: RouteFlavors = { traditional: false, expressions: true } +const TRADITIONAL_EXPRESSIONS: RouteFlavors = { traditional: true, expressions: true } + +const formatRouteFlavors = (routeFlavors?: RouteFlavors): string => { + return routeFlavors ? [...routeFlavors.traditional ? ['trad'] : [], ...routeFlavors.expressions ? ['expr'] : []].join('+') || 'none' : 'default' +} + describe('', { viewportHeight: 700, viewportWidth: 700 }, () => { describe('Kong Manager', () => { const interceptKM = (params?: { @@ -66,420 +75,738 @@ describe('', { viewportHeight: 700, viewportWidth: 700 }, () => { ).as('updateRoute') } - it('should show create form', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - }, - }) + /** + * Stub the POST and PATCH requests with mocked responses where `kind` marks the type of route + * being created/edited. This uses the validation steps that are similar to the backend to simply + * verify that the mutually exclusive fields are not included. + */ + const stubCreateEdit = () => { + const handler: RouteHandler = (req) => { + const { body } = req + + // only verify mutually exclusive fields + const hasExpressionsFields = Object.hasOwnProperty.call(body, 'expression') + const hasTraditionalFields = ['hosts', 'paths', 'headers', 'methods', 'snis', 'sources', 'destinations'] + .some((prop) => Object.hasOwnProperty.call(body, prop)) + + req.reply({ + statusCode: 400, + body: { + kind: hasExpressionsFields ? 'expr' : hasTraditionalFields ? 'trad' : undefined, + }, + }) + } - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.get('.kong-ui-entities-route-form form').should('be.visible') - - // button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // form fields - general - cy.getTestId('route-form-name').should('be.visible') - cy.getTestId('route-form-service-id').should('be.visible') - cy.getTestId('route-form-tags').should('be.visible') - - // advanced fields - cy.getTestId('collapse-trigger-content').should('be.visible').click() - cy.getTestId('route-form-path-handling').should('be.visible') - cy.getTestId('route-form-http-redirect-status-code').should('be.visible') - cy.getTestId('route-form-regex-priority').should('be.visible') - cy.getTestId('route-form-strip-path').should('be.visible') - cy.getTestId('route-form-preserve-host').should('be.visible') - cy.getTestId('route-form-request-buffering').should('be.visible') - cy.getTestId('route-form-response-buffering').should('be.visible') - - // routing rules fields - cy.getTestId('route-form-protocols').should('be.visible') - - // paths - cy.getTestId('route-form-paths-input-1').should('be.visible') - cy.getTestId('add-paths').should('be.visible').click() - cy.getTestId('route-form-paths-input-2').should('be.visible') - cy.getTestId('remove-paths').first().should('be.visible').click() - cy.getTestId('route-form-paths-input-2').should('not.exist') - - cy.getTestId('route-form-paths-input-1').should('be.visible') - cy.getTestId('remove-paths').first().should('be.visible').click() - cy.get('.route-form-routing-rules-selector-options').should('be.visible') - - // snis - cy.getTestId('routing-rule-snis').should('be.visible').click() - cy.getTestId('route-form-snis-input-1').should('be.visible') - cy.getTestId('add-snis').should('be.visible').click() - cy.getTestId('route-form-snis-input-2').should('be.visible') - cy.getTestId('remove-snis').first().should('be.visible').click() - cy.getTestId('route-form-snis-input-2').should('not.exist') - - // hosts - cy.getTestId('routing-rule-hosts').should('be.visible').click() - cy.getTestId('route-form-hosts-input-1').should('be.visible') - cy.getTestId('add-hosts').should('be.visible').click() - cy.getTestId('route-form-hosts-input-2').should('be.visible') - cy.getTestId('remove-hosts').first().should('be.visible').click() - cy.getTestId('route-form-hosts-input-2').should('not.exist') - - // methods and custom methods - cy.getTestId('routing-rule-methods').should('be.visible').click() - cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'true') - cy.getTestId('get-method-toggle').should('exist') - cy.getTestId('post-method-toggle').should('exist') - cy.getTestId('put-method-toggle').should('exist') - cy.getTestId('custom-method-toggle').should('exist').check({ force: true }) - cy.getTestId('route-form-custom-method-input-1').should('be.visible') - cy.getTestId('add-custom-method').should('be.visible').click() - cy.getTestId('route-form-custom-method-input-2').should('be.visible') - cy.getTestId('remove-custom-method').first().should('be.visible').click() - cy.getTestId('route-form-custom-method-input-2').should('not.exist') - cy.getTestId('remove-methods').should('be.visible').click() - cy.getTestId('get-method-toggle').should('not.exist') - cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'false') - - // headers - cy.getTestId('routing-rule-headers').should('be.visible').click() - cy.getTestId('route-form-headers-name-input-1').should('be.visible') - cy.getTestId('route-form-headers-values-input-1').should('be.visible') - cy.getTestId('add-headers').should('be.visible').click() - cy.getTestId('route-form-headers-name-input-2').should('be.visible') - cy.getTestId('route-form-headers-values-input-2').should('be.visible') - cy.getTestId('remove-headers').first().should('be.visible').click() - cy.getTestId('route-form-headers-name-input-2').should('not.exist') - cy.getTestId('route-form-headers-values-input-2').should('not.exist') + cy.intercept('POST', `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/routes`, handler).as('createRoute') + cy.intercept('PATCH', `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/routes/*`, handler).as('editRoute') + } - cy.getTestId('route-form-protocols').click({ force: true }) - cy.get("[data-testid='select-item-tcp,tls,udp']").click() - cy.getTestId('routing-rule-paths').should('not.exist') - cy.getTestId('routing-rule-hosts').should('not.exist') - cy.getTestId('routing-rule-methods').should('not.exist') - cy.getTestId('routing-rule-headers').should('not.exist') - - // sources - cy.getTestId('routing-rule-sources').should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-1').should('be.visible') - cy.getTestId('route-form-sources-port-input-1').should('be.visible') - cy.getTestId('add-sources').should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-2').should('be.visible') - cy.getTestId('route-form-sources-port-input-2').should('be.visible') - cy.getTestId('remove-sources').first().should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-2').should('not.exist') - cy.getTestId('route-form-sources-port-input-2').should('not.exist') - - // destinations - cy.getTestId('routing-rule-destinations').should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-1').should('be.visible') - cy.getTestId('route-form-destinations-port-input-1').should('be.visible') - cy.getTestId('add-destinations').should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-2').should('be.visible') - cy.getTestId('route-form-destinations-port-input-2').should('be.visible') - cy.getTestId('remove-destinations').first().should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-2').should('not.exist') - cy.getTestId('route-form-destinations-port-input-2').should('not.exist') - }) + // Tests 4 possible RouteFlavors: , , , + for (const routeFlavors of [undefined, TRADITIONAL_ONLY, EXPRESSIONS_ONLY, TRADITIONAL_EXPRESSIONS]) { + const configTabs = `tabs=${formatRouteFlavors(routeFlavors)}` - it('should correctly handle button state - create', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - }, + it(`should show create form, ${configTabs}`, () => { + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.get('.kong-ui-entities-route-form form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') + + // base + base advanced fields + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-name').should('be.visible') + cy.getTestId('route-form-service-id').should('be.visible') + cy.getTestId('route-form-tags').should('be.visible') + cy.getTestId('route-form-protocols').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to trad tab + cy.get('#traditional-tab').click() + } // else: we will be on the trad tab by default + + if (routeFlavors?.traditional) { + // base advanced fields + cy.getTestId('route-form-http-redirect-status-code').should('be.visible') + cy.getTestId('route-form-preserve-host').should('be.visible') + cy.getTestId('route-form-strip-path').should('be.visible') + cy.getTestId('route-form-request-buffering').should('be.visible') + cy.getTestId('route-form-response-buffering').should('be.visible') + + // other advanced fields + cy.getTestId('route-form-path-handling').should('be.visible') + cy.getTestId('route-form-regex-priority').should('be.visible') + + // paths + cy.getTestId('route-form-paths-input-1').should('be.visible') + cy.getTestId('add-paths').should('be.visible').click() + cy.getTestId('route-form-paths-input-2').should('be.visible') + cy.getTestId('remove-paths').first().should('be.visible').click() + cy.getTestId('route-form-paths-input-2').should('not.exist') + + cy.getTestId('route-form-paths-input-1').should('be.visible') + cy.getTestId('remove-paths').first().should('be.visible').click() + cy.get('.route-form-routing-rules-selector-options').should('be.visible') + + // snis + cy.getTestId('routing-rule-snis').should('be.visible').click() + cy.getTestId('route-form-snis-input-1').should('be.visible') + cy.getTestId('add-snis').should('be.visible').click() + cy.getTestId('route-form-snis-input-2').should('be.visible') + cy.getTestId('remove-snis').first().should('be.visible').click() + cy.getTestId('route-form-snis-input-2').should('not.exist') + + // hosts + cy.getTestId('routing-rule-hosts').should('be.visible').click() + cy.getTestId('route-form-hosts-input-1').should('be.visible') + cy.getTestId('add-hosts').should('be.visible').click() + cy.getTestId('route-form-hosts-input-2').should('be.visible') + cy.getTestId('remove-hosts').first().should('be.visible').click() + cy.getTestId('route-form-hosts-input-2').should('not.exist') + + // methods and custom methods + cy.getTestId('routing-rule-methods').should('be.visible').click() + cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'true') + cy.getTestId('get-method-toggle').should('exist') + cy.getTestId('post-method-toggle').should('exist') + cy.getTestId('put-method-toggle').should('exist') + cy.getTestId('custom-method-toggle').should('exist').check({ force: true }) + cy.getTestId('route-form-custom-method-input-1').should('be.visible') + cy.getTestId('add-custom-method').should('be.visible').click() + cy.getTestId('route-form-custom-method-input-2').should('be.visible') + cy.getTestId('remove-custom-method').first().should('be.visible').click() + cy.getTestId('route-form-custom-method-input-2').should('not.exist') + cy.getTestId('remove-methods').should('be.visible').click() + cy.getTestId('get-method-toggle').should('not.exist') + cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'false') + + // headers + cy.getTestId('routing-rule-headers').should('be.visible').click() + cy.getTestId('route-form-headers-name-input-1').should('be.visible') + cy.getTestId('route-form-headers-values-input-1').should('be.visible') + cy.getTestId('add-headers').should('be.visible').click() + cy.getTestId('route-form-headers-name-input-2').should('be.visible') + cy.getTestId('route-form-headers-values-input-2').should('be.visible') + cy.getTestId('remove-headers').first().should('be.visible').click() + cy.getTestId('route-form-headers-name-input-2').should('not.exist') + cy.getTestId('route-form-headers-values-input-2').should('not.exist') + + cy.getTestId('route-form-protocols').click({ force: true }) + cy.get("[data-testid='select-item-tcp,tls,udp']").click() + cy.getTestId('routing-rule-paths').should('not.exist') + cy.getTestId('routing-rule-hosts').should('not.exist') + cy.getTestId('routing-rule-methods').should('not.exist') + cy.getTestId('routing-rule-headers').should('not.exist') + + // sources + cy.getTestId('routing-rule-sources').should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-1').should('be.visible') + cy.getTestId('route-form-sources-port-input-1').should('be.visible') + cy.getTestId('add-sources').should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-2').should('be.visible') + cy.getTestId('route-form-sources-port-input-2').should('be.visible') + cy.getTestId('remove-sources').first().should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-2').should('not.exist') + cy.getTestId('route-form-sources-port-input-2').should('not.exist') + + // destinations + cy.getTestId('routing-rule-destinations').should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-1').should('be.visible') + cy.getTestId('route-form-destinations-port-input-1').should('be.visible') + cy.getTestId('add-destinations').should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-2').should('be.visible') + cy.getTestId('route-form-destinations-port-input-2').should('be.visible') + cy.getTestId('remove-destinations').first().should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-2').should('not.exist') + cy.getTestId('route-form-destinations-port-input-2').should('not.exist') + } + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to expr tab + cy.get('#expressions-tab').click() + } + + if (routeFlavors?.expressions) { + // negative: traditional fields should not exist + cy.getTestId('route-form-path-handling').should('not.exist') + cy.getTestId('route-form-regex-priority').should('not.exist') + cy.getTestId('route-form-paths-input-1').should('not.exist') + cy.get('.route-form-routing-rules-selector-options').should('not.exist') + + // expressions editor + cy.get('.expression-editor .monaco-editor').should('be.visible') + + // base advanced fields + cy.getTestId('route-form-http-redirect-status-code').should('be.visible') + cy.getTestId('route-form-preserve-host').should('be.visible') + cy.getTestId('route-form-strip-path').should('be.visible') + cy.getTestId('route-form-request-buffering').should('be.visible') + cy.getTestId('route-form-response-buffering').should('be.visible') + } }) - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // default button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // enables save when required fields have values - // form fields - general - cy.getTestId('route-form-name').should('be.visible') - - // paths - cy.getTestId('route-form-paths-input-1').type(route.paths[0]) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-paths-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // snis - cy.getTestId('routing-rule-snis').click() - cy.getTestId('route-form-snis-input-1').type('sni') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-snis-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // hosts - cy.getTestId('routing-rule-hosts').click() - cy.getTestId('route-form-hosts-input-1').type('host') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-hosts-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // methods and custom methods - cy.getTestId('routing-rule-methods').click() - cy.getTestId('get-method-toggle').check({ force: true }) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('get-method-toggle').uncheck({ force: true }) - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('custom-method-toggle').check({ force: true }) - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('route-form-custom-method-input-1').type('castom') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-custom-method-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // headers - cy.getTestId('routing-rule-headers').click() - cy.getTestId('route-form-headers-name-input-1').type(Object.keys(route.headers)[0]) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-headers-name-input-1').clear() - cy.getTestId('route-form-headers-values-input-1').type(route.headers.Header1[0]) - cy.getTestId('form-submit').should('be.disabled') - - cy.getTestId('route-form-protocols').click({ force: true }) - cy.get("[data-testid='select-item-tcp,tls,udp']").click() - - // sources - cy.getTestId('routing-rule-sources').click() - cy.getTestId('route-form-sources-ip-input-1').type('127.0.0.1') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-sources-ip-input-1').clear() - cy.getTestId('route-form-sources-port-input-1').type('8080') - cy.getTestId('form-submit').should('be.disabled') - - // destinations - cy.getTestId('routing-rule-destinations').click() - cy.getTestId('route-form-destinations-ip-input-1').type('127.0.0.2') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-destinations-ip-input-1').clear() - cy.getTestId('route-form-destinations-port-input-1').type('8000') - cy.getTestId('form-submit').should('be.disabled') - }) - - it('should show edit form', () => { - interceptKM() - interceptKMServices() + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // only test when both trad & expr tabs present + it('should show tooltips', () => { + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + configTabTooltips: { + traditional: 'For traditional routes', + expressions: 'For expressions routes', + }, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.get('.kong-ui-entities-route-form form').should('be.visible') + + cy.get('#traditional-tab .route-form-config-tabs-tooltip').should('contain.text', 'For traditional routes') + cy.get('#expressions-tab .route-form-config-tabs-tooltip').should('contain.text', 'For expressions routes') + }) + } + + if (!routeFlavors || routeFlavors?.traditional) { + // only test when there is trad tab + it(`should correctly handle button state - create traditional, ${configTabs}`, () => { + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to trad tab + cy.get('#traditional-tab').click() + } // else: we will be on the trad tab by default + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') + + // enables save when required fields have values + // form fields - general + cy.getTestId('route-form-name').should('be.visible') + + // paths + cy.getTestId('route-form-paths-input-1').type(route.paths[0]) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-paths-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // snis + cy.getTestId('routing-rule-snis').click() + cy.getTestId('route-form-snis-input-1').type('sni') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-snis-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // hosts + cy.getTestId('routing-rule-hosts').click() + cy.getTestId('route-form-hosts-input-1').type('host') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-hosts-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // methods and custom methods + cy.getTestId('routing-rule-methods').click() + cy.getTestId('get-method-toggle').check({ force: true }) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('get-method-toggle').uncheck({ force: true }) + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('custom-method-toggle').check({ force: true }) + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('route-form-custom-method-input-1').type('castom') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-custom-method-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // headers + cy.getTestId('routing-rule-headers').click() + cy.getTestId('route-form-headers-name-input-1').type(Object.keys(route.headers)[0]) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-headers-name-input-1').clear() + cy.getTestId('route-form-headers-values-input-1').type(route.headers.Header1[0]) + cy.getTestId('form-submit').should('be.disabled') + + cy.getTestId('route-form-protocols').click({ force: true }) + cy.get("[data-testid='select-item-tcp,tls,udp']").click() + + // sources + cy.getTestId('routing-rule-sources').click() + cy.getTestId('route-form-sources-ip-input-1').type('127.0.0.1') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-sources-ip-input-1').clear() + cy.getTestId('route-form-sources-port-input-1').type('8080') + cy.getTestId('form-submit').should('be.disabled') + + // destinations + cy.getTestId('routing-rule-destinations').click() + cy.getTestId('route-form-destinations-ip-input-1').type('127.0.0.2') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-destinations-ip-input-1').clear() + cy.getTestId('route-form-destinations-port-input-1').type('8000') + cy.getTestId('form-submit').should('be.disabled') + }) + } // if !routeFlavors || routeFlavors?.traditional + + if (routeFlavors?.expressions) { + // only test when there is expr tab + it(`should correctly handle button state - create expressions, ${configTabs}`, () => { + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to expr tab + cy.get('#expressions-tab').click() + } // else: we will be on expr tab by default + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // enables save when required fields have values + // form fields - general + cy.getTestId('route-form-name').should('be.visible') + cy.getTestId('form-submit').should('be.disabled') + + // the editor shows invalid because it is empty + cy.get('.expression-editor').should('have.class', 'invalid') + + // type a valid expression + cy.get('.monaco-editor').first().as('monacoEditor').click() + cy.get('@monacoEditor').type('http.path == "/kong"') + + // it should be no longer invalid + cy.get('.expression-editor').should('not.have.class', 'invalid') + // and the submit button is enabled + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + + // delete the last character + cy.get('@monacoEditor').type('{backspace}') + + // invalid again + cy.get('.expression-editor').should('have.class', 'invalid') + // but the submit button is still enabled because we let the server handle uncaught errors + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + }) + } // if routeFlavors?.expressions + + if (!routeFlavors || routeFlavors?.traditional) { + // only test when there is trad tab + it(`should show edit form, traditional ${configTabs}`, () => { + interceptKM() + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: route.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#traditional-tab').should('have.class', 'active') + } + + // form fields + cy.getTestId('route-form-name').should('have.value', route.name) + cy.getTestId('route-form-service-id').should('have.value', route.service.id) + cy.getTestId('route-form-tags').should('have.value', route.tags.join(', ')) + + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-path-handling').should('have.value', route.path_handling) + cy.getTestId('route-form-regex-priority').should('have.value', route.regex_priority) + cy.getTestId('route-form-strip-path').should(`${route.strip_path ? '' : 'not.'}be.checked`) + cy.getTestId('route-form-preserve-host').should(`${route.preserve_host ? '' : 'not.'}be.checked`) + + cy.getTestId('route-form-paths-input-1').should('have.value', route.paths[0]) + cy.getTestId('route-form-paths-input-2').should('have.value', route.paths[1]) + cy.getTestId(`${route.methods[0].toLowerCase()}-method-toggle`).should('be.checked') + cy.getTestId(`${route.methods[1].toLowerCase()}-method-toggle`).should('be.checked') + cy.getTestId('route-form-headers-name-input-1').should('have.value', Object.keys(route.headers)[0]) + cy.getTestId('route-form-headers-values-input-1').should('have.value', route.headers.Header1.join(',')) + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // switch to expr tab + cy.get('#expressions-tab').click() + // should not see the expression editor + cy.get('.expression-editor').should('not.exist') + // should be reminded that the route type cannot be changed + cy.getTestId('route-config-type-immutable-alert').should('contain.text', 'cannot be changed after creation') + } + }) + + it(`should correctly handle button state - edit traditional, ${configTabs}`, () => { + interceptKM() + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: route.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#traditional-tab').should('have.class', 'active') + } + + cy.getTestId('routing-rules-warning').should('not.exist') + + // enables save when form has changes + cy.getTestId('route-form-service-id').click({ force: true }) + cy.get("[data-testid='select-item-2']").click() + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('remove-methods').click() + cy.getTestId('remove-paths').first().click() + cy.getTestId('remove-paths').click() + cy.getTestId('remove-headers').click() + cy.getTestId('routing-rules-warning').should('be.visible') + cy.getTestId('form-submit').should('be.disabled') + }) + } // if !routeFlavors || routeFlavors?.traditional + + if (routeFlavors?.expressions) { + // only test when there is trad tab + it(`should show edit form, expressions ${configTabs}`, () => { + interceptKM({ mockData: routeExpressions }) + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: routeExpressions.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // expr tab should be active by default + cy.get('#expressions-tab').should('have.class', 'active') + } + + // form fields + cy.getTestId('route-form-name').should('have.value', routeExpressions.name) + cy.getTestId('route-form-service-id').should('have.value', routeExpressions.service.id) + cy.getTestId('route-form-tags').should('have.value', routeExpressions.tags.join(', ')) + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + cy.getTestId('collapse-trigger-content').click() + // switch to trad tab + cy.get('#traditional-tab').click() + // should not see trad fields + cy.getTestId('route-form-path-handling').should('not.exist') + cy.getTestId('route-form-regex-priority').should('not.exist') + // should be reminded that the route type cannot be changed + cy.getTestId('route-config-type-immutable-alert').should('contain.text', 'cannot be changed after creation') + } + }) + + it(`should correctly handle button state - edit expressions, ${configTabs}`, () => { + interceptKM({ mockData: routeExpressions }) + interceptKMServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: routeExpressions.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#expressions-tab').should('have.class', 'active') + cy.getTestId('route-form-expressions-editor-loader-loading').should('not.exist') + } + + // enables save when form has changes + cy.getTestId('route-form-service-id').click({ force: true }) + cy.get("[data-testid='select-item-2']").click() + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + + // type a valid expression + cy.get('.monaco-editor').first().as('monacoEditor').click() + // delete the last character + cy.get('@monacoEditor').type('{backspace}') + + // the editor should become invalid + cy.get('.expression-editor').should('have.class', 'invalid') + // but the submit button is still enabled because we let the server handle uncaught errors + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + }) + } // if routeFlavors?.expressions + + it(`should handle error state - failed to load route, ${configTabs}`, () => { + interceptKMServices() + + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/routes/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getRoute') - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - routeId: route.id, - }, - }) + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: route.id, + routeFlavors, + }, + }) - cy.wait('@getRoute') - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.wait('@getRoute') + cy.wait('@getServices') - // button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // form fields - cy.getTestId('route-form-name').should('have.value', route.name) - cy.getTestId('route-form-service-id').should('have.value', route.service.id) - cy.getTestId('route-form-tags').should('have.value', route.tags.join(', ')) - cy.getTestId('route-form-paths-input-1').should('have.value', route.paths[0]) - cy.getTestId('route-form-paths-input-2').should('have.value', route.paths[1]) - cy.getTestId(`${route.methods[0].toLowerCase()}-method-toggle`).should('be.checked') - cy.getTestId(`${route.methods[1].toLowerCase()}-method-toggle`).should('be.checked') - cy.getTestId('route-form-headers-name-input-1').should('have.value', Object.keys(route.headers)[0]) - cy.getTestId('route-form-headers-values-input-1').should('have.value', route.headers.Header1.join(',')) - - cy.getTestId('collapse-trigger-content').click() - cy.getTestId('route-form-path-handling').should('have.value', route.path_handling) - cy.getTestId('route-form-regex-priority').should('have.value', route.regex_priority) - cy.getTestId('route-form-strip-path').should(`${route.strip_path ? '' : 'not.'}be.checked`) - cy.getTestId('route-form-preserve-host').should(`${route.preserve_host ? '' : 'not.'}be.checked`) - }) + cy.get('.kong-ui-entities-route-form').should('be.visible') - it('should correctly handle button state - edit', () => { - interceptKM() - interceptKMServices() + // error state is displayed + cy.getTestId('form-fetch-error').should('be.visible') - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - routeId: route.id, - }, + // buttons and form hidden + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.get('.kong-ui-entities-route-form form').should('not.exist') }) - cy.wait('@getRoute') - cy.wait('@getServices') - - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // default button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('routing-rules-warning').should('not.exist') - - // enables save when form has changes - cy.getTestId('route-form-service-id').click({ force: true }) - cy.get("[data-testid='select-item-2']").click() - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('remove-methods').click() - cy.getTestId('remove-paths').first().click() - cy.getTestId('remove-paths').click() - cy.getTestId('remove-headers').click() - cy.getTestId('routing-rules-warning').should('be.visible') - cy.getTestId('form-submit').should('be.disabled') - }) - - it('should handle error state - failed to load route', () => { - interceptKMServices() + it(`should allow exact match filtering of services, ${configTabs}`, () => { + interceptKMServices() - cy.intercept( - { - method: 'GET', - url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/routes/*`, - }, - { - statusCode: 404, - body: {}, - }, - ).as('getRoute') - - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - routeId: route.id, - }, - }) - - cy.wait('@getRoute') - cy.wait('@getServices') - - cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + }, + }) - // error state is displayed - cy.getTestId('form-fetch-error').should('be.visible') + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') - // buttons and form hidden - cy.getTestId('form-cancel').should('not.exist') - cy.getTestId('form-submit').should('not.exist') - cy.get('.kong-ui-entities-route-form form').should('not.exist') - }) + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') - it('should allow exact match filtering of services', () => { - interceptKMServices() + // search + cy.getTestId('route-form-service-id').should('be.visible') + cy.getTestId('route-form-service-id').type(services[1].name) - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - }, + // click kselect item + cy.getTestId(`select-item-${services[1].id}`).should('be.visible') + cy.get(`[data-testid="select-item-${services[1].id}"] button`).click() + cy.getTestId('route-form-service-id').should('have.value', services[1].id) }) - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // search - cy.getTestId('route-form-service-id').should('be.visible') - cy.getTestId('route-form-service-id').type(services[1].name) - - // click kselect item - cy.getTestId(`select-item-${services[1].id}`).should('be.visible') - cy.get(`[data-testid="select-item-${services[1].id}"] button`).click() - cy.getTestId('route-form-service-id').should('have.value', services[1].id) - }) + it(`should handle error state - failed to load services, ${configTabs}`, () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/services*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getServices') - it('should handle error state - failed to load services', () => { - cy.intercept( - { - method: 'GET', - url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/services*`, - }, - { - statusCode: 500, - body: {}, - }, - ).as('getServices') + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeFlavors, + }, + }) - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - }, + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.getTestId('form-error').should('be.visible') }) - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.getTestId('form-error').should('be.visible') - }) + it(`should correctly render with all props and slot content, ${configTabs}`, () => { + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + serviceId: services[0].id, + hideSectionsInfo: true, + hideNameField: true, + showTagsFiledUnderAdvanced: true, + routeFlavors, + }, + slots: { + 'form-actions': '', + }, + }) - it('should correctly render with all props and slot content', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - serviceId: services[0].id, - hideSectionsInfo: true, - hideNameField: true, - showTagsFiledUnderAdvanced: true, - }, - slots: { - 'form-actions': '', - }, - }) + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.get('.kong-ui-entities-route-form form').should('be.visible') - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.get('.kong-ui-entities-route-form form').should('be.visible') + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') - // name field should be hidden when hideNameField is true - cy.getTestId('route-form-name').should('not.exist') + // name field should be hidden when hideNameField is true + cy.getTestId('route-form-name').should('not.exist') - // tags field should render under advanced fields - cy.getTestId('route-form-tags').should('not.be.visible') - cy.getTestId('collapse-trigger-content').click() - cy.getTestId('route-form-tags').should('be.visible') + // tags field should render under advanced fields + cy.getTestId('route-form-tags').should('not.be.visible') + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-tags').should('be.visible') - // service id field should be hidden when serviceId is provided - cy.getTestId('route-form-service-id').should('not.exist') + // service id field should be hidden when serviceId is provided + cy.getTestId('route-form-service-id').should('not.exist') - // sections info should be hidden when hideSectionsInfo is true - cy.get('.form-section-info sticky').should('not.exist') + // sections info should be hidden when hideSectionsInfo is true + cy.get('.form-section-info sticky').should('not.exist') - // default buttons should be replaced with slotted content - cy.getTestId('form-cancel').should('not.exist') - cy.getTestId('form-submit').should('not.exist') - cy.getTestId('slotted-cancel-button').should('be.visible') - cy.getTestId('slotted-submit-button').should('be.visible') - }) + // default buttons should be replaced with slotted content + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.getTestId('slotted-cancel-button').should('be.visible') + cy.getTestId('slotted-submit-button').should('be.visible') + }) - it('update event should be emitted when Route was edited', () => { - interceptKM() - interceptUpdate() + it(`update event should be emitted when Route was edited, ${configTabs}`, () => { + interceptKM() + interceptUpdate() - cy.mount(RouteForm, { - props: { - config: baseConfigKM, - routeId: route.id, - onUpdate: cy.spy().as('onUpdateSpy'), - }, - }).then(({ wrapper }) => wrapper) - .as('vueWrapper') + cy.mount(RouteForm, { + props: { + config: baseConfigKM, + routeId: route.id, + onUpdate: cy.spy().as('onUpdateSpy'), + routeFlavors, + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') - cy.wait('@getRoute') - cy.getTestId('route-form-tags').clear() - cy.getTestId('route-form-tags').type('tag1,tag2') + cy.wait('@getRoute') + cy.getTestId('route-form-tags').clear() + cy.getTestId('route-form-tags').type('tag1,tag2') - cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) - .vm.$emit('submit')) + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) - cy.wait('@updateRoute') + cy.wait('@updateRoute') - cy.get('@onUpdateSpy').should('have.been.calledOnce') - }) + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + } // for RouteFlavors[] it('should hide `ws` options when not supported', () => { cy.mount(RouteForm, { @@ -496,6 +823,9 @@ describe('', { viewportHeight: 700, viewportWidth: 700 }, () => { cy.get('.kong-ui-entities-route-form').should('be.visible') + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs').should('not.exist') + cy.getTestId('route-form-protocols').click({ force: true }) cy.getTestId('select-item-http').should('exist') cy.getTestId('select-item-ws').should('not.exist') @@ -549,417 +879,736 @@ describe('', { viewportHeight: 700, viewportWidth: 700 }, () => { ).as('updateRoute') } - it('should show create form', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - }, - }) + /** + * Stub the POST and PATCH requests with mocked responses where `kind` marks the type of route + * being created/edited. This uses the validation steps that are similar to the backend to simply + * verify that the mutually exclusive fields are not included. + */ + const stubCreateEdit = () => { + const handler: RouteHandler = (req) => { + const { body } = req + + // only verify mutually exclusive fields + const hasExpressionsFields = Object.hasOwnProperty.call(body, 'expression') + const hasTraditionalFields = ['hosts', 'paths', 'headers', 'methods', 'snis', 'sources', 'destinations'] + .some((prop) => Object.hasOwnProperty.call(body, prop)) + + req.reply({ + statusCode: 400, + body: { + kind: hasExpressionsFields ? 'expr' : hasTraditionalFields ? 'trad' : undefined, + }, + }) + } - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.get('.kong-ui-entities-route-form form').should('be.visible') - - // button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // form fields - general - cy.getTestId('route-form-name').should('be.visible') - cy.getTestId('route-form-service-id').should('be.visible') - cy.getTestId('route-form-tags').should('be.visible') - - // advanced fields - cy.getTestId('collapse-trigger-content').should('be.visible').click() - cy.getTestId('route-form-path-handling').should('be.visible') - cy.getTestId('route-form-http-redirect-status-code').should('be.visible') - cy.getTestId('route-form-regex-priority').should('be.visible') - cy.getTestId('route-form-strip-path').should('be.visible') - cy.getTestId('route-form-preserve-host').should('be.visible') - - // routing rules fields - cy.getTestId('route-form-protocols').should('be.visible') - - // paths - cy.getTestId('route-form-paths-input-1').should('be.visible') - cy.getTestId('add-paths').should('be.visible').click() - cy.getTestId('route-form-paths-input-2').should('be.visible') - cy.getTestId('remove-paths').first().should('be.visible').click() - cy.getTestId('route-form-paths-input-2').should('not.exist') - - cy.getTestId('route-form-paths-input-1').should('be.visible') - cy.getTestId('remove-paths').first().should('be.visible').click() - cy.get('.route-form-routing-rules-selector-options').should('be.visible') - - // snis - cy.getTestId('routing-rule-snis').should('be.visible').click() - cy.getTestId('route-form-snis-input-1').should('be.visible') - cy.getTestId('add-snis').should('be.visible').click() - cy.getTestId('route-form-snis-input-2').should('be.visible') - cy.getTestId('remove-snis').first().should('be.visible').click() - cy.getTestId('route-form-snis-input-2').should('not.exist') - - // hosts - cy.getTestId('routing-rule-hosts').should('be.visible').click() - cy.getTestId('route-form-hosts-input-1').should('be.visible') - cy.getTestId('add-hosts').should('be.visible').click() - cy.getTestId('route-form-hosts-input-2').should('be.visible') - cy.getTestId('remove-hosts').first().should('be.visible').click() - cy.getTestId('route-form-hosts-input-2').should('not.exist') - - // methods and custom methods - cy.getTestId('routing-rule-methods').should('be.visible').click() - cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'true') - cy.getTestId('get-method-toggle').should('exist') - cy.getTestId('post-method-toggle').should('exist') - cy.getTestId('put-method-toggle').should('exist') - cy.getTestId('custom-method-toggle').should('exist').check({ force: true }) - cy.getTestId('route-form-custom-method-input-1').should('be.visible') - cy.getTestId('add-custom-method').should('be.visible').click() - cy.getTestId('route-form-custom-method-input-2').should('be.visible') - cy.getTestId('remove-custom-method').first().should('be.visible').click() - cy.getTestId('route-form-custom-method-input-2').should('not.exist') - cy.getTestId('remove-methods').should('be.visible').click() - cy.getTestId('get-method-toggle').should('not.exist') - cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'false') - - // headers - cy.getTestId('routing-rule-headers').should('be.visible').click() - cy.getTestId('route-form-headers-name-input-1').should('be.visible') - cy.getTestId('route-form-headers-values-input-1').should('be.visible') - cy.getTestId('add-headers').should('be.visible').click() - cy.getTestId('route-form-headers-name-input-2').should('be.visible') - cy.getTestId('route-form-headers-values-input-2').should('be.visible') - cy.getTestId('remove-headers').first().should('be.visible').click() - cy.getTestId('route-form-headers-name-input-2').should('not.exist') - cy.getTestId('route-form-headers-values-input-2').should('not.exist') + cy.intercept('POST', `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/routes`, handler).as('createRoute') + cy.intercept('PUT', `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/routes/*`, handler).as('editRoute') + } - cy.getTestId('route-form-protocols').click({ force: true }) - cy.get("[data-testid='select-item-tcp,tls,udp']").click() - cy.getTestId('routing-rule-paths').should('not.exist') - cy.getTestId('routing-rule-hosts').should('not.exist') - cy.getTestId('routing-rule-methods').should('not.exist') - cy.getTestId('routing-rule-headers').should('not.exist') - - // sources - cy.getTestId('routing-rule-sources').should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-1').should('be.visible') - cy.getTestId('route-form-sources-port-input-1').should('be.visible') - cy.getTestId('add-sources').should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-2').should('be.visible') - cy.getTestId('route-form-sources-port-input-2').should('be.visible') - cy.getTestId('remove-sources').first().should('be.visible').click() - cy.getTestId('route-form-sources-ip-input-2').should('not.exist') - cy.getTestId('route-form-sources-port-input-2').should('not.exist') - - // destinations - cy.getTestId('routing-rule-destinations').should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-1').should('be.visible') - cy.getTestId('route-form-destinations-port-input-1').should('be.visible') - cy.getTestId('add-destinations').should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-2').should('be.visible') - cy.getTestId('route-form-destinations-port-input-2').should('be.visible') - cy.getTestId('remove-destinations').first().should('be.visible').click() - cy.getTestId('route-form-destinations-ip-input-2').should('not.exist') - cy.getTestId('route-form-destinations-port-input-2').should('not.exist') - }) + // Tests 2 possible RouteFlavors: , + for (const routeFlavors of [undefined, TRADITIONAL_ONLY]) { + const configTabs = `tabs=${formatRouteFlavors(routeFlavors)}` - it('should correctly handle button state - create', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - }, + it(`should show create form, ${configTabs}`, () => { + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.get('.kong-ui-entities-route-form form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') + + // base + base advanced fields + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-name').should('be.visible') + cy.getTestId('route-form-service-id').should('be.visible') + cy.getTestId('route-form-tags').should('be.visible') + cy.getTestId('route-form-protocols').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to trad tab + cy.get('#traditional-tab').click() + } // else: we will be on the trad tab by default + + if (routeFlavors?.traditional) { + // base advanced fields + cy.getTestId('route-form-http-redirect-status-code').should('be.visible') + cy.getTestId('route-form-preserve-host').should('be.visible') + cy.getTestId('route-form-strip-path').should('be.visible') + cy.getTestId('route-form-request-buffering').should('be.visible') + cy.getTestId('route-form-response-buffering').should('be.visible') + + // other advanced fields + cy.getTestId('route-form-path-handling').should('be.visible') + cy.getTestId('route-form-regex-priority').should('be.visible') + + // paths + cy.getTestId('route-form-paths-input-1').should('be.visible') + cy.getTestId('add-paths').should('be.visible').click() + cy.getTestId('route-form-paths-input-2').should('be.visible') + cy.getTestId('remove-paths').first().should('be.visible').click() + cy.getTestId('route-form-paths-input-2').should('not.exist') + + cy.getTestId('route-form-paths-input-1').should('be.visible') + cy.getTestId('remove-paths').first().should('be.visible').click() + cy.get('.route-form-routing-rules-selector-options').should('be.visible') + + // snis + cy.getTestId('routing-rule-snis').should('be.visible').click() + cy.getTestId('route-form-snis-input-1').should('be.visible') + cy.getTestId('add-snis').should('be.visible').click() + cy.getTestId('route-form-snis-input-2').should('be.visible') + cy.getTestId('remove-snis').first().should('be.visible').click() + cy.getTestId('route-form-snis-input-2').should('not.exist') + + // hosts + cy.getTestId('routing-rule-hosts').should('be.visible').click() + cy.getTestId('route-form-hosts-input-1').should('be.visible') + cy.getTestId('add-hosts').should('be.visible').click() + cy.getTestId('route-form-hosts-input-2').should('be.visible') + cy.getTestId('remove-hosts').first().should('be.visible').click() + cy.getTestId('route-form-hosts-input-2').should('not.exist') + + // methods and custom methods + cy.getTestId('routing-rule-methods').should('be.visible').click() + cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'true') + cy.getTestId('get-method-toggle').should('exist') + cy.getTestId('post-method-toggle').should('exist') + cy.getTestId('put-method-toggle').should('exist') + cy.getTestId('custom-method-toggle').should('exist').check({ force: true }) + cy.getTestId('route-form-custom-method-input-1').should('be.visible') + cy.getTestId('add-custom-method').should('be.visible').click() + cy.getTestId('route-form-custom-method-input-2').should('be.visible') + cy.getTestId('remove-custom-method').first().should('be.visible').click() + cy.getTestId('route-form-custom-method-input-2').should('not.exist') + cy.getTestId('remove-methods').should('be.visible').click() + cy.getTestId('get-method-toggle').should('not.exist') + cy.getTestId('routing-rule-methods').should('have.attr', 'aria-disabled').and('eq', 'false') + + // headers + cy.getTestId('routing-rule-headers').should('be.visible').click() + cy.getTestId('route-form-headers-name-input-1').should('be.visible') + cy.getTestId('route-form-headers-values-input-1').should('be.visible') + cy.getTestId('add-headers').should('be.visible').click() + cy.getTestId('route-form-headers-name-input-2').should('be.visible') + cy.getTestId('route-form-headers-values-input-2').should('be.visible') + cy.getTestId('remove-headers').first().should('be.visible').click() + cy.getTestId('route-form-headers-name-input-2').should('not.exist') + cy.getTestId('route-form-headers-values-input-2').should('not.exist') + + cy.getTestId('route-form-protocols').click({ force: true }) + cy.get("[data-testid='select-item-tcp,tls,udp']").click() + cy.getTestId('routing-rule-paths').should('not.exist') + cy.getTestId('routing-rule-hosts').should('not.exist') + cy.getTestId('routing-rule-methods').should('not.exist') + cy.getTestId('routing-rule-headers').should('not.exist') + + // sources + cy.getTestId('routing-rule-sources').should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-1').should('be.visible') + cy.getTestId('route-form-sources-port-input-1').should('be.visible') + cy.getTestId('add-sources').should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-2').should('be.visible') + cy.getTestId('route-form-sources-port-input-2').should('be.visible') + cy.getTestId('remove-sources').first().should('be.visible').click() + cy.getTestId('route-form-sources-ip-input-2').should('not.exist') + cy.getTestId('route-form-sources-port-input-2').should('not.exist') + + // destinations + cy.getTestId('routing-rule-destinations').should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-1').should('be.visible') + cy.getTestId('route-form-destinations-port-input-1').should('be.visible') + cy.getTestId('add-destinations').should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-2').should('be.visible') + cy.getTestId('route-form-destinations-port-input-2').should('be.visible') + cy.getTestId('remove-destinations').first().should('be.visible').click() + cy.getTestId('route-form-destinations-ip-input-2').should('not.exist') + cy.getTestId('route-form-destinations-port-input-2').should('not.exist') + } // if routeFlavors?.traditional + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to expr tab + cy.get('#expressions-tab').click() + } + + if (routeFlavors?.expressions) { + // negative: traditional fields should not exist + cy.getTestId('route-form-path-handling').should('not.exist') + cy.getTestId('route-form-regex-priority').should('not.exist') + cy.getTestId('route-form-paths-input-1').should('not.exist') + cy.get('.route-form-routing-rules-selector-options').should('not.exist') + + // expressions editor + cy.get('.expression-editor .monaco-editor').should('be.visible') + + // base advanced fields + cy.getTestId('route-form-http-redirect-status-code').should('be.visible') + cy.getTestId('route-form-preserve-host').should('be.visible') + cy.getTestId('route-form-strip-path').should('be.visible') + cy.getTestId('route-form-request-buffering').should('be.visible') + cy.getTestId('route-form-response-buffering').should('be.visible') + } // if routeFlavors?.expressions }) - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // default button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // enables save when required fields have values - // form fields - general - cy.getTestId('route-form-name').should('be.visible') - - // paths - cy.getTestId('route-form-paths-input-1').type(route.paths[0]) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-paths-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // snis - cy.getTestId('routing-rule-snis').click() - cy.getTestId('route-form-snis-input-1').type('sni') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-snis-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // hosts - cy.getTestId('routing-rule-hosts').click() - cy.getTestId('route-form-hosts-input-1').type('host') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-hosts-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // methods and custom methods - cy.getTestId('routing-rule-methods').click() - cy.getTestId('get-method-toggle').check({ force: true }) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('get-method-toggle').uncheck({ force: true }) - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('custom-method-toggle').check({ force: true }) - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('route-form-custom-method-input-1').type('castom') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-custom-method-input-1').clear() - cy.getTestId('form-submit').should('be.disabled') - - // headers - cy.getTestId('routing-rule-headers').click() - cy.getTestId('route-form-headers-name-input-1').type(Object.keys(route.headers)[0]) - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-headers-name-input-1').clear() - cy.getTestId('route-form-headers-values-input-1').type(route.headers.Header1[0]) - cy.getTestId('form-submit').should('be.disabled') - - cy.getTestId('route-form-protocols').click({ force: true }) - cy.get("[data-testid='select-item-tcp,tls,udp']").click() - - // sources - cy.getTestId('routing-rule-sources').click() - cy.getTestId('route-form-sources-ip-input-1').type('127.0.0.1') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-sources-ip-input-1').clear() - cy.getTestId('route-form-sources-port-input-1').type('8080') - cy.getTestId('form-submit').should('be.disabled') - - // destinations - cy.getTestId('routing-rule-destinations').click() - cy.getTestId('route-form-destinations-ip-input-1').type('127.0.0.2') - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('route-form-destinations-ip-input-1').clear() - cy.getTestId('route-form-destinations-port-input-1').type('8000') - cy.getTestId('form-submit').should('be.disabled') - }) - - it('should show edit form', () => { - interceptKonnect() - interceptKonnectServices() + if (!routeFlavors || routeFlavors?.traditional) { + // only test when there is trad tab + it(`should correctly handle button state - create traditional, ${configTabs}`, () => { + interceptKonnectServices() + stubCreateEdit() - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - routeId: route.id, - }, + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to trad tab + cy.get('#traditional-tab').click() + } // else: we will be on the trad tab by default + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // config tabs is hidden when there is only one tab + cy.getTestId('route-form-config-tabs') + .should(routeFlavors?.traditional && routeFlavors?.expressions ? 'be.visible' : 'not.exist') + + // enables save when required fields have values + // form fields - general + cy.getTestId('route-form-name').should('be.visible') + + // paths + cy.getTestId('route-form-paths-input-1').type(route.paths[0]) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-paths-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // snis + cy.getTestId('routing-rule-snis').click() + cy.getTestId('route-form-snis-input-1').type('sni') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-snis-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // hosts + cy.getTestId('routing-rule-hosts').click() + cy.getTestId('route-form-hosts-input-1').type('host') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-hosts-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // methods and custom methods + cy.getTestId('routing-rule-methods').click() + cy.getTestId('get-method-toggle').check({ force: true }) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('get-method-toggle').uncheck({ force: true }) + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('custom-method-toggle').check({ force: true }) + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('route-form-custom-method-input-1').type('castom') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-custom-method-input-1').clear() + cy.getTestId('form-submit').should('be.disabled') + + // headers + cy.getTestId('routing-rule-headers').click() + cy.getTestId('route-form-headers-name-input-1').type(Object.keys(route.headers)[0]) + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-headers-name-input-1').clear() + cy.getTestId('route-form-headers-values-input-1').type(route.headers.Header1[0]) + cy.getTestId('form-submit').should('be.disabled') + + cy.getTestId('route-form-protocols').click({ force: true }) + cy.get("[data-testid='select-item-tcp,tls,udp']").click() + + // sources + cy.getTestId('routing-rule-sources').click() + cy.getTestId('route-form-sources-ip-input-1').type('127.0.0.1') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-sources-ip-input-1').clear() + cy.getTestId('route-form-sources-port-input-1').type('8080') + cy.getTestId('form-submit').should('be.disabled') + + // destinations + cy.getTestId('routing-rule-destinations').click() + cy.getTestId('route-form-destinations-ip-input-1').type('127.0.0.2') + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('route-form-destinations-ip-input-1').clear() + cy.getTestId('route-form-destinations-port-input-1').type('8000') + cy.getTestId('form-submit').should('be.disabled') + }) + } // if !routeFlavors || routeFlavors?.traditional + + if (routeFlavors?.expressions) { + // only test when there is expr tab + it(`should correctly handle button state - create expressions, ${configTabs}`, () => { + interceptKonnectServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeFlavors, + }, + }) + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad + expr 2 tabs + // switch to expr tab + cy.get('#expressions-tab').click() + } // else: we will be on expr tab by default + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + // enables save when required fields have values + // form fields - general + cy.getTestId('route-form-name').should('be.visible') + cy.getTestId('form-submit').should('be.disabled') + + // the editor shows invalid because it is empty + cy.get('.expression-editor').should('have.class', 'invalid') + + // type a valid expression + cy.get('.monaco-editor').first().as('monacoEditor').click() + cy.get('@monacoEditor').type('http.path == "/kong"') + + // it should be no longer invalid + cy.get('.expression-editor').should('not.have.class', 'invalid') + // and the submit button is enabled + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + + // delete the last character + cy.get('@monacoEditor').type('{backspace}') + + // invalid again + cy.get('.expression-editor').should('have.class', 'invalid') + // but the submit button is still enabled because we let the server handle uncaught errors + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@createRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + }) + } // if routeFlavors?.expressions + + if (!routeFlavors || routeFlavors?.traditional) { + // only test when there is trad tab + it(`should show edit form, traditional ${configTabs}`, () => { + interceptKonnect() + interceptKonnectServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: route.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#traditional-tab').should('have.class', 'active') + } + + // form fields + cy.getTestId('route-form-name').should('have.value', route.name) + cy.getTestId('route-form-service-id').should('have.value', route.service.id) + cy.getTestId('route-form-tags').should('have.value', route.tags.join(', ')) + + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-path-handling').should('have.value', route.path_handling) + cy.getTestId('route-form-regex-priority').should('have.value', route.regex_priority) + cy.getTestId('route-form-strip-path').should(`${route.strip_path ? '' : 'not.'}be.checked`) + cy.getTestId('route-form-preserve-host').should(`${route.preserve_host ? '' : 'not.'}be.checked`) + + cy.getTestId('route-form-paths-input-1').should('have.value', route.paths[0]) + cy.getTestId('route-form-paths-input-2').should('have.value', route.paths[1]) + cy.getTestId(`${route.methods[0].toLowerCase()}-method-toggle`).should('be.checked') + cy.getTestId(`${route.methods[1].toLowerCase()}-method-toggle`).should('be.checked') + cy.getTestId('route-form-headers-name-input-1').should('have.value', Object.keys(route.headers)[0]) + cy.getTestId('route-form-headers-values-input-1').should('have.value', route.headers.Header1.join(',')) + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // switch to expr tab + cy.get('#expressions-tab').click() + // should not see the expression editor + cy.get('.expression-editor').should('not.exist') + // should be reminded that the route type cannot be changed + cy.getTestId('route-config-type-immutable-alert').should('contain.text', 'cannot be changed after creation') + } + }) + + it(`should correctly handle button state - edit traditional, ${configTabs}`, () => { + interceptKonnect() + interceptKonnectServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: route.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#traditional-tab').should('have.class', 'active') + } + + cy.getTestId('routing-rules-warning').should('not.exist') + + // enables save when form has changes + cy.getTestId('route-form-service-id').click({ force: true }) + cy.get("[data-testid='select-item-2']").click() + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'trad') + + cy.getTestId('remove-methods').click() + cy.getTestId('remove-paths').first().click() + cy.getTestId('remove-paths').click() + cy.getTestId('remove-headers').click() + cy.getTestId('routing-rules-warning').should('be.visible') + cy.getTestId('form-submit').should('be.disabled') + }) + } // if !routeFlavors || routeFlavors?.traditional + + if (routeFlavors?.expressions) { + // only test when there is trad tab + it(`should show edit form, expressions ${configTabs}`, () => { + interceptKonnect({ mockData: routeExpressions }) + interceptKonnectServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: routeExpressions.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // expr tab should be active by default + cy.get('#expressions-tab').should('have.class', 'active') + } + + // form fields + cy.getTestId('route-form-name').should('have.value', routeExpressions.name) + cy.getTestId('route-form-service-id').should('have.value', routeExpressions.service.id) + cy.getTestId('route-form-tags').should('have.value', routeExpressions.tags.join(', ')) + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + cy.getTestId('collapse-trigger-content').click() + // switch to trad tab + cy.get('#traditional-tab').click() + // should not see trad fields + cy.getTestId('route-form-path-handling').should('not.exist') + cy.getTestId('route-form-regex-priority').should('not.exist') + // should be reminded that the route type cannot be changed + cy.getTestId('route-config-type-immutable-alert').should('contain.text', 'cannot be changed after creation') + } + }) + + it(`should correctly handle button state - edit expressions, ${configTabs}`, () => { + interceptKonnect({ mockData: routeExpressions }) + interceptKonnectServices() + stubCreateEdit() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: routeExpressions.id, + routeFlavors, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + + if (routeFlavors?.traditional && routeFlavors?.expressions) { + // trad tab should be active by default + cy.get('#expressions-tab').should('have.class', 'active') + } + + // enables save when form has changes + cy.getTestId('route-form-service-id').click({ force: true }) + cy.get("[data-testid='select-item-2']").click() + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + + // type a valid expression + cy.get('.monaco-editor').first().as('monacoEditor').click() + // delete the last character + cy.get('@monacoEditor').type('{backspace}') + + // the editor should become invalid + cy.get('.expression-editor').should('have.class', 'invalid') + // but the submit button is still enabled because we let the server handle uncaught errors + cy.getTestId('form-submit').should('be.enabled').click() + cy.wait('@editRoute').then((res) => res.response?.body?.kind).should('eq', 'expr') + }) + } // if routeFlavors?.expressions + + it('should correctly handle button state - edit', () => { + interceptKonnect() + interceptKonnectServices() + + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: route.id, + }, + }) + + cy.wait('@getRoute') + cy.wait('@getServices') + + cy.get('.kong-ui-entities-route-form').should('be.visible') + + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + cy.getTestId('routing-rules-warning').should('not.exist') + + // enables save when form has changes + cy.getTestId('route-form-service-id').click({ force: true }) + cy.get("[data-testid='select-item-2']").click() + cy.getTestId('form-submit').should('be.enabled') + cy.getTestId('remove-methods').click() + cy.getTestId('remove-paths').first().click() + cy.getTestId('remove-paths').click() + cy.getTestId('remove-headers').click() + cy.getTestId('routing-rules-warning').should('be.visible') + cy.getTestId('form-submit').should('be.disabled') }) - cy.wait('@getRoute') - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - - // form fields - cy.getTestId('route-form-name').should('have.value', route.name) - cy.getTestId('route-form-service-id').should('have.value', route.service.id) - cy.getTestId('route-form-tags').should('have.value', route.tags.join(', ')) - cy.getTestId('route-form-paths-input-1').should('have.value', route.paths[0]) - cy.getTestId('route-form-paths-input-2').should('have.value', route.paths[1]) - cy.getTestId(`${route.methods[0].toLowerCase()}-method-toggle`).should('be.checked') - cy.getTestId(`${route.methods[0].toLowerCase()}-method-toggle`).should('be.checked') - cy.getTestId('route-form-headers-name-input-1').should('have.value', Object.keys(route.headers)[0]) - cy.getTestId('route-form-headers-values-input-1').should('have.value', route.headers.Header1.join(',')) - - cy.getTestId('collapse-trigger-content').click() - cy.getTestId('route-form-path-handling').should('have.value', route.path_handling) - cy.getTestId('route-form-regex-priority').should('have.value', route.regex_priority) - cy.getTestId('route-form-strip-path').should(`${route.strip_path ? '' : 'not.'}be.checked`) - cy.getTestId('route-form-preserve-host').should(`${route.preserve_host ? '' : 'not.'}be.checked`) - }) + it('should handle error state - failed to load route', () => { + interceptKonnectServices() - it('should correctly handle button state - edit', () => { - interceptKonnect() - interceptKonnectServices() - - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - routeId: route.id, - }, - }) + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/routes/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getRoute') - cy.wait('@getRoute') - cy.wait('@getServices') + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: route.id, + }, + }) - cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.wait('@getRoute') + cy.wait('@getServices') - // default button state - cy.getTestId('form-cancel').should('be.visible') - cy.getTestId('form-submit').should('be.visible') - cy.getTestId('form-cancel').should('be.enabled') - cy.getTestId('form-submit').should('be.disabled') - cy.getTestId('routing-rules-warning').should('not.exist') - - // enables save when form has changes - cy.getTestId('route-form-service-id').click({ force: true }) - cy.get("[data-testid='select-item-2']").click() - cy.getTestId('form-submit').should('be.enabled') - cy.getTestId('remove-methods').click() - cy.getTestId('remove-paths').first().click() - cy.getTestId('remove-paths').click() - cy.getTestId('remove-headers').click() - cy.getTestId('routing-rules-warning').should('be.visible') - cy.getTestId('form-submit').should('be.disabled') - }) + cy.get('.kong-ui-entities-route-form').should('be.visible') - it('should handle error state - failed to load route', () => { - interceptKonnectServices() + // error state is displayed + cy.getTestId('form-fetch-error').should('be.visible') - cy.intercept( - { - method: 'GET', - url: `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/routes/*`, - }, - { - statusCode: 404, - body: {}, - }, - ).as('getRoute') - - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - routeId: route.id, - }, + // buttons and form hidden + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.get('.kong-ui-entities-route-form form').should('not.exist') }) - cy.wait('@getRoute') - cy.wait('@getServices') + it('should allow exact match filtering of certs', () => { + interceptKonnectServices() - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // error state is displayed - cy.getTestId('form-fetch-error').should('be.visible') + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + }, + }) - // buttons and form hidden - cy.getTestId('form-cancel').should('not.exist') - cy.getTestId('form-submit').should('not.exist') - cy.get('.kong-ui-entities-route-form form').should('not.exist') - }) + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') - it('should allow exact match filtering of certs', () => { - interceptKonnectServices() + // search + cy.getTestId('route-form-service-id').should('be.visible') + cy.getTestId('route-form-service-id').type(services[1].name) - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - }, + // click kselect item + cy.getTestId(`select-item-${services[1].id}`).should('be.visible') + cy.get(`[data-testid="select-item-${services[1].id}"] button`).click() + cy.getTestId('route-form-service-id').should('have.value', services[1].id) }) - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') - - // search - cy.getTestId('route-form-service-id').should('be.visible') - cy.getTestId('route-form-service-id').type(services[1].name) - - // click kselect item - cy.getTestId(`select-item-${services[1].id}`).should('be.visible') - cy.get(`[data-testid="select-item-${services[1].id}"] button`).click() - cy.getTestId('route-form-service-id').should('have.value', services[1].id) - }) + it('should handle error state - failed to load services', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/services*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getServices') - it('should handle error state - failed to load services', () => { - cy.intercept( - { - method: 'GET', - url: `${baseConfigKonnect.apiBaseUrl}/v2/control-planes/${baseConfigKonnect.controlPlaneId}/core-entities/services*`, - }, - { - statusCode: 500, - body: {}, - }, - ).as('getServices') + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + }, + }) - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - }, + cy.wait('@getServices') + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.getTestId('form-error').should('be.visible') }) - cy.wait('@getServices') - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.getTestId('form-error').should('be.visible') - }) - - it('should correctly render with all props and slot content', () => { - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - serviceId: services[0].id, - hideSectionsInfo: true, - hideNameField: true, - showTagsFiledUnderAdvanced: true, - }, - slots: { - 'form-actions': '', - }, - }) + it('should correctly render with all props and slot content', () => { + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + serviceId: services[0].id, + hideSectionsInfo: true, + hideNameField: true, + showTagsFiledUnderAdvanced: true, + }, + slots: { + 'form-actions': '', + }, + }) - cy.get('.kong-ui-entities-route-form').should('be.visible') - cy.get('.kong-ui-entities-route-form form').should('be.visible') + cy.get('.kong-ui-entities-route-form').should('be.visible') + cy.get('.kong-ui-entities-route-form form').should('be.visible') - // name field should be hidden when hideNameField is true - cy.getTestId('route-form-name').should('not.exist') + // name field should be hidden when hideNameField is true + cy.getTestId('route-form-name').should('not.exist') - // tags field should render under advanced fields - cy.getTestId('route-form-tags').should('not.be.visible') - cy.getTestId('collapse-trigger-content').click() - cy.getTestId('route-form-tags').should('be.visible') + // tags field should render under advanced fields + cy.getTestId('route-form-tags').should('not.be.visible') + cy.getTestId('collapse-trigger-content').click() + cy.getTestId('route-form-tags').should('be.visible') - // service id field should be hidden when serviceId is provided - cy.getTestId('route-form-service-id').should('not.exist') + // service id field should be hidden when serviceId is provided + cy.getTestId('route-form-service-id').should('not.exist') - // sections info should be hidden when hideSectionsInfo is true - cy.get('.form-section-info sticky').should('not.exist') + // sections info should be hidden when hideSectionsInfo is true + cy.get('.form-section-info sticky').should('not.exist') - // default buttons should be replaced with slotted content - cy.getTestId('form-cancel').should('not.exist') - cy.getTestId('form-submit').should('not.exist') - cy.getTestId('slotted-cancel-button').should('be.visible') - cy.getTestId('slotted-submit-button').should('be.visible') - }) + // default buttons should be replaced with slotted content + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.getTestId('slotted-cancel-button').should('be.visible') + cy.getTestId('slotted-submit-button').should('be.visible') + }) - it('update event should be emitted when Route was edited', () => { - interceptKonnect() - interceptUpdate() + it('update event should be emitted when Route was edited', () => { + interceptKonnect() + interceptUpdate() - cy.mount(RouteForm, { - props: { - config: baseConfigKonnect, - routeId: route.id, - onUpdate: cy.spy().as('onUpdateSpy'), - }, - }).then(({ wrapper }) => wrapper) - .as('vueWrapper') + cy.mount(RouteForm, { + props: { + config: baseConfigKonnect, + routeId: route.id, + onUpdate: cy.spy().as('onUpdateSpy'), + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') - cy.wait('@getRoute') - cy.getTestId('route-form-tags').clear() - cy.getTestId('route-form-tags').type('tag1,tag2') + cy.wait('@getRoute') + cy.getTestId('route-form-tags').clear() + cy.getTestId('route-form-tags').type('tag1,tag2') - cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) - .vm.$emit('submit')) + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) - cy.wait('@updateRoute') + cy.wait('@updateRoute') - cy.get('@onUpdateSpy').should('have.been.calledOnce') - }) + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + } // for RouteFlavors[] }) }) diff --git a/packages/entities/entities-routes/src/components/RouteForm.vue b/packages/entities/entities-routes/src/components/RouteForm.vue index 51ab640136..ab9a09ca2d 100644 --- a/packages/entities/entities-routes/src/components/RouteForm.vue +++ b/packages/entities/entities-routes/src/components/RouteForm.vue @@ -4,10 +4,10 @@ :can-submit="isFormValid && changesExist" :config="config" :edit-id="routeId" - :error-message="form.errorMessage || fetchServicesErrorMessage" + :error-message="state.errorMessage || fetchServicesErrorMessage" :fetch-url="fetchUrl" :form-fields="getPayload" - :is-readonly="form.isReadonly" + :is-readonly="state.isReadonly" @cancel="cancelHandler" @fetch:error="fetchErrorHandler" @fetch:success="updateFormValues" @@ -22,17 +22,17 @@ >
+ - -