diff --git a/cypress/composables/logs.ts b/cypress/composables/logs.ts new file mode 100644 index 0000000000..3131913ca4 --- /dev/null +++ b/cypress/composables/logs.ts @@ -0,0 +1,41 @@ +/** + * Accessors + */ + +export function getLogEntryAtRow(rowIndex: number) { + return cy.getByTestId('logs-overview-body').find('[role=treeitem]').eq(rowIndex); +} + +export function getInputTableRows() { + return cy.getByTestId('log-details-input').find('table tr'); +} + +export function getInputTbodyCell(row: number, col: number) { + return cy.getByTestId('log-details-input').find('table tr').eq(row).find('td').eq(col); +} + +/** + * Actions + */ + +export function openLogsPanel() { + cy.getByTestId('logs-overview-header').click(); +} + +export function clickLogEntryAtRow(rowIndex: number) { + getLogEntryAtRow(rowIndex).click(); +} + +export function toggleInputPanel() { + cy.getByTestId('log-details-header').contains('Input').click(); +} + +export function clickOpenNdvAtRow(rowIndex: number) { + getLogEntryAtRow(rowIndex).realHover(); + getLogEntryAtRow(rowIndex).find('[aria-label="Open..."]').click(); +} + +export function setInputDisplayMode(mode: 'table') { + cy.getByTestId('log-details-input').realHover(); + cy.getByTestId('log-details-input').findChildByTestId(`radio-button-${mode}`).click(); +} diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index d633e2c6cf..cee9817a54 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -32,6 +32,10 @@ export function getInputPanel() { return cy.getByTestId('ndv-input-panel'); } +export function getInputSelect() { + return cy.getByTestId('ndv-input-select').find('input'); +} + export function getMainPanel() { return cy.getByTestId('node-parameters'); } @@ -53,11 +57,19 @@ export function getResourceLocatorInput(paramName: string) { } export function getInputPanelDataContainer() { - return getInputPanel().getByTestId('ndv-data-container'); + return getInputPanel().findChildByTestId('ndv-data-container'); +} + +export function getInputTableRows() { + return getInputPanelDataContainer().find('table tr'); +} + +export function getInputTbodyCell(row: number, col: number) { + return getInputTableRows().eq(row).find('td').eq(col); } export function getOutputPanelDataContainer() { - return getOutputPanel().getByTestId('ndv-data-container'); + return getOutputPanel().findChildByTestId('ndv-data-container'); } export function getOutputTableRows() { @@ -278,7 +290,7 @@ export function assertInlineExpressionValid() { } export function hoverInputItemByText(text: string) { - return getInputPanelDataContainer().contains(text).trigger('mouseover', { force: true }); + return getInputPanelDataContainer().contains(text).realHover(); } export function verifyInputHoverState(expectedText: string) { @@ -296,5 +308,5 @@ export function verifyOutputHoverState(expectedText: string) { } export function resetHoverState() { - getBackToCanvasButton().trigger('mouseover'); + getBackToCanvasButton().realHover(); } diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index f94f91e82f..7344362e97 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -25,10 +25,12 @@ export type EndpointType = * Getters */ -export function executeWorkflowAndWait() { +export function executeWorkflowAndWait(waitForSuccessBannerToDisappear = true) { cy.get('[data-test-id="execute-workflow-button"]').click(); cy.contains('Workflow executed successfully', { timeout: 4000 }).should('be.visible'); - cy.contains('Workflow executed successfully', { timeout: 10000 }).should('not.exist'); + if (waitForSuccessBannerToDisappear) { + cy.contains('Workflow executed successfully', { timeout: 10000 }).should('not.exist'); + } } export function getCanvas() { diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index e75fddf2b9..457557773a 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -87,23 +87,18 @@ describe('NDV', () => { ndv.actions.selectInputNode('Set1'); ndvComposables.verifyInputHoverState('1000'); - ndv.actions.dragMainPanelToRight(); - - ndvComposables.resetHoverState(); + ndvComposables.hoverInputItemByText('1000'); ndvComposables.verifyOutputHoverState('1000'); - // BUG(ADO-3469): Expression preview is not updated when input node is changed it uses the old value - // ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.actions.selectInputNode('Sort'); - ndv.actions.dragMainPanelToLeft(); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndvComposables.resetHoverState(); ndvComposables.verifyInputHoverState('1111'); - ndv.actions.dragMainPanelToRight(); - - ndvComposables.resetHoverState(); + ndvComposables.hoverInputItemByText('1111'); ndvComposables.verifyOutputHoverState('1111'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); diff --git a/cypress/e2e/50-logs.cy.ts b/cypress/e2e/50-logs.cy.ts index 7461fee85d..dcc736aec9 100644 --- a/cypress/e2e/50-logs.cy.ts +++ b/cypress/e2e/50-logs.cy.ts @@ -1,4 +1,57 @@ +import * as logs from '../composables/logs'; +import * as ndv from '../composables/ndv'; +import * as workflow from '../composables/workflow'; +import Workflow from '../fixtures/Workflow_if.json'; + describe('Logs', () => { - // TODO: the test can be written without AI nodes once https://linear.app/n8n/issue/SUG-39 is implemented - it('should open NDV with the run index that corresponds to clicked log entry'); + beforeEach(() => { + cy.overrideSettings({ logsView: { enabled: true } }); + }); + + it('should show input and output data of correct run index and branch', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + workflow.executeWorkflowAndWait(false); + + logs.clickLogEntryAtRow(2); // Run #1 of 'Edit Fields' node; input is 'Code' node + logs.toggleInputPanel(); + logs.setInputDisplayMode('table'); + logs.getInputTableRows().should('have.length', 11); + logs.getInputTbodyCell(1, 0).should('contain.text', '0'); + logs.getInputTbodyCell(10, 0).should('contain.text', '9'); + logs.clickOpenNdvAtRow(2); + ndv.getInputSelect().should('have.value', 'Code '); + ndv.getInputTableRows().should('have.length', 11); + ndv.getInputTbodyCell(1, 0).should('contain.text', '0'); + ndv.getInputTbodyCell(10, 0).should('contain.text', '9'); + ndv.getOutputRunSelectorInput().should('have.value', '1 of 3 (10 items)'); + + ndv.clickGetBackToCanvas(); + + logs.clickLogEntryAtRow(4); // Run #2 of 'Edit Fields' node; input is false branch of 'If' node + logs.getInputTableRows().should('have.length', 6); + logs.getInputTbodyCell(1, 0).should('contain.text', '5'); + logs.getInputTbodyCell(5, 0).should('contain.text', '9'); + logs.clickOpenNdvAtRow(4); + ndv.getInputSelect().should('have.value', 'If '); + ndv.getInputTableRows().should('have.length', 6); + ndv.getInputTbodyCell(1, 0).should('contain.text', '5'); + ndv.getInputTbodyCell(5, 0).should('contain.text', '9'); + ndv.getOutputRunSelectorInput().should('have.value', '2 of 3 (5 items)'); + + ndv.clickGetBackToCanvas(); + + logs.clickLogEntryAtRow(5); // Run #3 of 'Edit Fields' node; input is true branch of 'If' node + logs.getInputTableRows().should('have.length', 6); + logs.getInputTbodyCell(1, 0).should('contain.text', '0'); + logs.getInputTbodyCell(5, 0).should('contain.text', '4'); + logs.clickOpenNdvAtRow(5); + ndv.getInputSelect().should('have.value', 'If '); + ndv.getInputTableRows().should('have.length', 6); + ndv.getInputTbodyCell(1, 0).should('contain.text', '0'); + ndv.getInputTbodyCell(5, 0).should('contain.text', '4'); + ndv.getOutputRunSelectorInput().should('have.value', '3 of 3 (5 items)'); + }); }); diff --git a/cypress/fixtures/Workflow_if.json b/cypress/fixtures/Workflow_if.json new file mode 100644 index 0000000000..21dacb3538 --- /dev/null +++ b/cypress/fixtures/Workflow_if.json @@ -0,0 +1,127 @@ +{ + "nodes": [ + { + "parameters": { + "rule": { + "interval": [{}] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [-900, 60], + "id": "e6b8fc7c-442e-4283-a0cd-604dc7c9e816", + "name": "Schedule Trigger" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "553f50d9-5023-433f-8f62-eebc9c9e2269", + "leftValue": "={{ $json.data }}", + "rightValue": 5, + "operator": { + "type": "number", + "operation": "lt" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [-460, 135], + "id": "f5c96b5b-9e22-4348-a258-fdb0417f5ff5", + "name": "If" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "71475f04-571e-4e99-bdf8-adff367533fb", + "name": "data", + "value": "={{ $json.data }}", + "type": "number" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-240, 60], + "id": "2a6fc40d-5d8c-4c35-bf53-ee910267619f", + "name": "Edit Fields" + }, + { + "parameters": { + "jsCode": "return Array.from({length:10}).map((_,i)=>({data:i}))" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [-680, 60], + "id": "12ae07e7-be34-43b6-806b-4c24be169ee6", + "name": "Code" + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "If": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "instanceId": "db1f26b45a71ad9a8df79dde8d35bf1be13616c3b23eb55be8ecf642dd31500c" + } +} diff --git a/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButton.vue b/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButton.vue index 34ac7c23ad..0b50e6f78e 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButton.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButton.vue @@ -4,15 +4,15 @@ interface RadioButtonProps { value: string; active?: boolean; disabled?: boolean; - size?: 'small' | 'medium'; - noPadding?: boolean; + size?: 'small' | 'small-medium' | 'medium'; + square?: boolean; } withDefaults(defineProps(), { active: false, disabled: false, size: 'medium', - noPadding: false, + square: false, }); defineSlots<{ default?: {} }>(); @@ -26,7 +26,7 @@ defineSlots<{ default?: {} }>(); 'n8n-radio-button': true, [$style.container]: true, [$style.hoverable]: !disabled, - [$style.noPadding]: noPadding, + [$style.square]: square, }" :aria-checked="active" > @@ -76,8 +76,10 @@ defineSlots<{ default?: {} }>(); cursor: pointer; user-select: none; - .noPadding & { - padding-inline: 0; + .square & { + display: flex; + align-items: center; + justify-content: center; } } @@ -89,12 +91,33 @@ defineSlots<{ default?: {} }>(); height: 26px; font-size: var(--font-size-2xs); padding: 0 var(--spacing-xs); + + .square & { + width: 26px; + padding: 0; + } +} + +.small-medium { + height: 22px; + font-size: var(--font-size-3xs); + padding: 0 var(--spacing-2xs); + + .square & { + width: 22px; + padding: 0; + } } .small { font-size: var(--font-size-3xs); height: 15px; padding: 0 var(--spacing-4xs); + + .square & { + width: 15px; + padding: 0; + } } .active { diff --git a/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButtons.vue b/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButtons.vue index f5c454677d..e5959f0ebc 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButtons.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nRadioButtons/RadioButtons.vue @@ -11,14 +11,16 @@ interface RadioButtonsProps { modelValue?: Value; options?: RadioOption[]; /** @default medium */ - size?: 'small' | 'medium'; + size?: 'small' | 'small-medium' | 'medium'; disabled?: boolean; + squareButtons?: boolean; } const props = withDefaults(defineProps(), { active: false, disabled: false, size: 'medium', + squareButtons: false, }); const emit = defineEmits<{ @@ -50,7 +52,7 @@ const onClick = ( :active="modelValue === option.value" :size="size" :disabled="disabled || option.disabled" - :no-padding="!!slots.option" + :square="squareButtons" @click.prevent.stop="onClick(option, $event)" > diff --git a/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue b/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue index fcd8c5f307..2735e95d0e 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue @@ -17,10 +17,12 @@ interface TabOptions { interface TabsProps { modelValue?: Value; options?: TabOptions[]; + size?: 'small' | 'medium'; } withDefaults(defineProps(), { options: () => [], + size: 'medium', }); const scrollPosition = ref(0); @@ -74,7 +76,7 @@ const scrollRight = () => scroll(50);