Files
openidea/e2e/databaseJsonValue.spec.ts
T
Vadim LazukandInf1mum 6c3e5ae469 feat(database): JSON-aware value editor for json cells (#2410)
Refs #2333

Editing json / jsonb / MySQL JSON cells showed one long plain text line, and invalid JSON only failed on Submit. The Value panel and the grid's ⇧Enter window now open the IDE's code editor for JSON cells, pretty-printed (big integers and duplicate keys preserved, large values formatted off the main thread) with highlighting, folding and find; validation as you type blocks Apply only for JSON columns; Reformat; Apply of an unchanged value creates no pending edit, and drafts survive opening the window. XML and bytea modes from #2333 remain.

Co-authored-by: Inf1mum <8135225+Inf1mum@users.noreply.github.com>
2026-11-10 05:22:56 +08:00

150 lines
6.6 KiB
TypeScript

import type { EditorView } from "@codemirror/view";
import { expect, test } from "./fixtures";
const compact = '{"name":"Ada","tags":["math","code"],"n":9007199254740993}';
for (const type of ["json", "text"]) {
test(`JSON value window preserves text and uses ${type} validation rules`, async ({ workbench: page }) => {
await page.evaluate(
async ({ compact, type }) => {
const reactPath = "/node_modules/.vite/deps/react.js";
const domPath = "/node_modules/.vite/deps/react-dom_client.js";
const viewPath = "/src/features/database/DataView.tsx";
const react = await import(reactPath);
const dom = await import(domPath);
const { createElement, useState } = react.default ?? react;
const { createRoot } = dom.default ?? dom;
const { DataView } = await import(viewPath);
const host = document.createElement("div");
host.style.cssText = "position:fixed;inset:100px;z-index:100;display:flex;background:var(--bg-main)";
document.body.append(host);
function Value() {
const [value, setValue] = useState(compact);
return createElement(DataView, {
columns: [{ name: "payload", type }],
rowCount: 1,
value: () => value,
editable: true,
panel: "value",
onPanelChange: () => {},
onEdit: (_row: number, _col: number, text: string) => setValue(text),
});
}
createRoot(host).render(createElement(Value));
},
{ compact, type },
);
const cell = page.getByRole("gridcell", { name: compact, exact: true });
await cell.click();
const panel = page.getByRole("complementary", { name: "Value panel" });
const editor = panel.locator(".cm-content");
await expect(editor).toContainText("9007199254740993");
await expect(panel.getByRole("button", { name: "Apply", exact: true })).toBeDisabled();
await cell.press("Shift+Enter");
const dialog = page.getByRole("dialog");
await expect(dialog.locator(".cm-content")).toBeFocused();
await expect
.poll(() =>
dialog.locator(".cm-editor").evaluate((editor) => {
const body = editor.closest("[data-dialog-body]")!;
return Math.abs(body.clientHeight - editor.clientHeight);
}),
)
.toBeLessThanOrEqual(4);
await expect(dialog.getByRole("button", { name: "Apply", exact: true })).toBeDisabled();
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(cell).toHaveText(compact);
await editor.evaluate((element) => {
const view = (element as HTMLElement & { cmTile: { view: EditorView } }).cmTile.view;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: '{"name":"Grace","n":9007199254740993}' },
});
view.focus();
});
await page.keyboard.press("Meta+Alt+L");
await expect(editor.locator(".cm-line")).toHaveCount(4);
await page.keyboard.press("Meta+f");
await expect(panel.locator(".cm-search")).toBeVisible();
await panel.locator(".cm-search button[name=close]").click();
await panel.getByRole("button", { name: "Open in Window", exact: true }).click();
await expect(dialog.locator(".cm-content")).toContainText("Grace");
await expect(dialog.getByRole("button", { name: "Apply", exact: true })).toBeEnabled();
await dialog.locator(".cm-content").evaluate((element) => {
const view = (element as HTMLElement & { cmTile: { view: EditorView } }).cmTile.view;
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "plain text" } });
});
const apply = dialog.getByRole("button", { name: "Apply", exact: true });
if (type === "json") {
await expect(apply).toBeDisabled();
await expect(dialog.getByRole("status")).toBeVisible();
} else {
await expect(apply).toBeEnabled();
await apply.click();
await expect(page.getByRole("gridcell", { name: "plain text", exact: true })).toBeVisible();
}
});
}
test("validates a large JSON value in a worker and applies the exact edited text", async ({ workbench: page }) => {
await page.evaluate(async () => {
const reactPath = "/node_modules/.vite/deps/react.js";
const domPath = "/node_modules/.vite/deps/react-dom_client.js";
const viewPath = "/src/features/database/JsonValueEditor.tsx";
const react = await import(reactPath);
const dom = await import(domPath);
const { createElement } = react.default ?? react;
const { createRoot } = dom.default ?? dom;
const { JsonValueWindow } = await import(viewPath);
const host = document.createElement("div");
document.body.append(host);
const result = document.createElement("output");
result.id = "applied-json";
document.body.append(result);
createRoot(host).render(
createElement(JsonValueWindow, {
title: "Edit payload",
initial: '{"large":"' + "x".repeat(5_000_000) + '"}',
validate: true,
onClose: () => {},
onApply: (text: string) => {
result.textContent = text;
},
}),
);
});
const dialog = page.getByRole("dialog", { name: "Edit payload" });
const editor = dialog.locator(".cm-content");
await expect(dialog.getByRole("button", { name: "Reformat", exact: true })).toBeEnabled();
await dialog.getByRole("button", { name: "Reformat", exact: true }).click();
await expect
.poll(() =>
editor.evaluate(
(element) =>
(element as HTMLElement & { cmTile: { view: EditorView } }).cmTile.view.state.doc.toString() ===
'{\n "large": "' + "x".repeat(5_000_000) + '"\n}',
),
)
.toBe(true);
await editor.evaluate((element) => {
const view = (element as HTMLElement & { cmTile: { view: EditorView } }).cmTile.view;
view.dispatch({ changes: { from: view.state.doc.length - 1, to: view.state.doc.length } });
});
await expect(dialog.getByRole("status")).toBeVisible();
await expect(dialog.getByRole("button", { name: "Apply", exact: true })).toBeDisabled();
await editor.evaluate((element) => {
const view = (element as HTMLElement & { cmTile: { view: EditorView } }).cmTile.view;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: '{\n "large": "' + "x".repeat(5_000_000) + '"\n}' },
});
});
await expect(dialog.getByRole("button", { name: "Apply", exact: true })).toBeEnabled();
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect
.poll(() =>
page
.locator("#applied-json")
.evaluate((element) => element.textContent === '{\n "large": "' + "x".repeat(5_000_000) + '"\n}'),
)
.toBe(true);
});