fix(editor): Render HTML in the log view (#17586)

This commit is contained in:
Suguru Inoue
2025-07-24 10:21:41 +02:00
committed by GitHub
parent 4713827813
commit 46635c5941
6 changed files with 89 additions and 26 deletions

View File

@@ -130,14 +130,18 @@ export const defaultNodeDescriptions = Object.values(defaultNodeTypes).map(
({ type }) => type.description,
) as INodeTypeDescription[];
const nodeTypes = mock<INodeTypes>({
getByName(nodeType) {
return defaultNodeTypes[nodeType].type;
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(defaultNodeTypes[nodeType].type, version);
},
});
export function createMockNodeTypes(data: INodeTypeData) {
return mock<INodeTypes>({
getByName(nodeType) {
return data[nodeType].type;
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(data[nodeType].type, version);
},
});
}
const nodeTypes = createMockNodeTypes(defaultNodeTypes);
export function createTestWorkflowObject({
id = uuid(),
@@ -148,6 +152,7 @@ export function createTestWorkflowObject({
staticData = {},
settings = {},
pinData = {},
...rest
}: {
id?: string;
name?: string;
@@ -157,6 +162,7 @@ export function createTestWorkflowObject({
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
nodeTypes?: INodeTypes;
} = {}) {
return new Workflow({
id,
@@ -167,7 +173,7 @@ export function createTestWorkflowObject({
staticData,
settings,
pinData,
nodeTypes,
nodeTypes: rest.nodeTypes ?? nodeTypes,
});
}

View File

@@ -112,3 +112,8 @@ Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
writable: true,
value: vi.fn(),
});
Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
writable: true,
value: vi.fn(),
});

View File

@@ -721,7 +721,6 @@ onMounted(() => {
});
if (props.paneType === 'output') {
setDisplayMode();
activatePane();
}
@@ -1231,6 +1230,10 @@ function init() {
if (isNDVV2.value) {
pageSize.value = RUN_DATA_DEFAULT_PAGE_SIZE;
}
if (props.paneType === 'output') {
setDisplayMode();
}
}
function closeBinaryDataDisplay() {
@@ -1337,14 +1340,14 @@ function enableNode() {
}
}
const shouldDisplayHtml = computed(
() =>
node.value?.type === HTML_NODE_TYPE &&
node.value.parameters.operation === 'generateHtmlTemplate',
);
function setDisplayMode() {
if (!activeNode.value) return;
const shouldDisplayHtml =
activeNode.value.type === HTML_NODE_TYPE &&
activeNode.value.parameters.operation === 'generateHtmlTemplate';
if (shouldDisplayHtml) {
if (shouldDisplayHtml.value) {
emit('displayModeChange', 'html');
}
}
@@ -1461,10 +1464,7 @@ defineExpose({ enterEditMode });
:value="displayMode"
:has-binary-data="binaryData.length > 0"
:pane-type="paneType"
:node-generates-html="
activeNode?.type === HTML_NODE_TYPE &&
activeNode.parameters.operation === 'generateHtmlTemplate'
"
:node-generates-html="shouldDisplayHtml"
:has-renderable-data="hasParsedAiContent"
@change="onDisplayModeChange"
/>

View File

@@ -1,19 +1,23 @@
import { fireEvent, within } from '@testing-library/vue';
import { fireEvent, waitFor, within } from '@testing-library/vue';
import { renderComponent } from '@/__tests__/render';
import LogDetailsPanel from './LogDetailsPanel.vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { h } from 'vue';
import {
createMockNodeTypes,
createTestNode,
createTestTaskData,
createTestWorkflow,
createTestWorkflowObject,
defaultNodeTypes,
mockLoadedNodeType,
} from '@/__tests__/mocks';
import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants';
import type { LogEntry } from '../logs.types';
import { createTestLogEntry } from '../__test__/mocks';
import { NodeConnectionTypes } from 'n8n-workflow';
import { HTML_NODE_TYPE } from '@/constants';
describe('LogDetailsPanel', () => {
let pinia: TestingPinia;
@@ -182,4 +186,47 @@ describe('LogDetailsPanel', () => {
),
).toBeInTheDocument();
});
it('should render output data in HTML mode for HTML node', async () => {
const nodeA = createTestNode({ name: 'A' });
const nodeB = createTestNode({
name: 'B',
type: HTML_NODE_TYPE,
});
const runDataA = createTestTaskData({ data: { [NodeConnectionTypes.Main]: [[{ json: {} }]] } });
const runDataB = createTestTaskData({
data: { [NodeConnectionTypes.Main]: [[{ json: { html: '<h1>Hi!</h1>' } }]] },
source: [{ previousNode: 'A' }],
});
const workflow = createTestWorkflowObject({
nodes: [nodeA, nodeB],
nodeTypes: createMockNodeTypes({
...defaultNodeTypes,
[HTML_NODE_TYPE]: mockLoadedNodeType(HTML_NODE_TYPE),
}),
});
const execution = { resultData: { runData: { A: [runDataA], B: [runDataB] } } };
const logA = createLogEntry({ node: nodeA, runData: runDataA, workflow, execution });
const logB = createLogEntry({ node: nodeB, runData: runDataB, workflow, execution });
// HACK: Setting parameters after creating workflow because validation removes parameters that are not define in node types.
nodeB.parameters = { operation: 'generateHtmlTemplate' };
const props = {
isOpen: true,
panels: LOG_DETAILS_PANEL_STATE.BOTH,
collapsingInputTableColumnName: null,
collapsingOutputTableColumnName: null,
};
const rendered = render({ ...props, logEntry: logB });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1));
await rendered.rerender({ ...props, logEntry: logA });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(0));
// Re-selecting node B should render HTML again
await rendered.rerender({ ...props, logEntry: logB });
await waitFor(() => expect(rendered.container.querySelectorAll('iframe')).toHaveLength(1));
});
});