🐛 Fix rename on non empty page (#9850)

This commit is contained in:
Eva Marco 2026-05-26 08:58:09 +02:00 committed by GitHub
parent 017f1d9994
commit 9439d63682
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 442 additions and 0 deletions

View File

@ -0,0 +1,308 @@
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Rename Page Test File",
"~:revn": 1,
"~:modified-at": "~m1713873823633",
"~:id": "~uaaaaaaaa-0000-0000-0000-000000000001",
"~:is-shared": false,
"~:version": 46,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713536343369",
"~:data": {
"~:pages": [
"~ubbbbbbbb-0000-0000-0000-000000000001",
"~ucccccccc-0000-0000-0000-000000000002",
"~udddddddd-0000-0000-0000-000000000003"
],
"~:pages-index": {
"~ubbbbbbbb-0000-0000-0000-000000000001": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{"~#point": {"~:x": 0, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0.01}},
{"~#point": {"~:x": 0, "~:y": 0.01}}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [{"~:fill-color": "#FFFFFF", "~:fill-opacity": 1}],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": ["~ueeeeeee0-0000-0000-0000-000000000001"]
}
},
"~ueeeeeee0-0000-0000-0000-000000000001": {
"~#shape": {
"~:y": 100,
"~:r1": 0,
"~:r2": 0,
"~:r3": 0,
"~:r4": 0,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 200,
"~:type": "~:rect",
"~:points": [
{"~#point": {"~:x": 100, "~:y": 100}},
{"~#point": {"~:x": 300, "~:y": 100}},
{"~#point": {"~:x": 300, "~:y": 300}},
{"~#point": {"~:x": 100, "~:y": 300}}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ueeeeeee0-0000-0000-0000-000000000001",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 100,
"~:proportion": 1.0,
"~:selrect": {
"~#rect": {
"~:x": 100,
"~:y": 100,
"~:width": 200,
"~:height": 200,
"~:x1": 100,
"~:y1": 100,
"~:x2": 300,
"~:y2": 300
}
},
"~:fills": [{"~:fill-color": "#B1B2B5", "~:fill-opacity": 1}],
"~:flip-x": null,
"~:ry": 0,
"~:height": 200,
"~:flip-y": null
}
}
},
"~:id": "~ubbbbbbbb-0000-0000-0000-000000000001",
"~:name": "Page 1"
},
"~ucccccccc-0000-0000-0000-000000000002": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{"~#point": {"~:x": 0, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0.01}},
{"~#point": {"~:x": 0, "~:y": 0.01}}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [{"~:fill-color": "#FFFFFF", "~:fill-opacity": 1}],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": []
}
}
},
"~:id": "~ucccccccc-0000-0000-0000-000000000002",
"~:name": "Page 2"
},
"~udddddddd-0000-0000-0000-000000000003": {
"~:options": {},
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{"~#point": {"~:x": 0, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0}},
{"~#point": {"~:x": 0.01, "~:y": 0.01}},
{"~#point": {"~:x": 0, "~:y": 0.01}}
],
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [{"~:fill-color": "#FFFFFF", "~:fill-opacity": 1}],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": []
}
}
},
"~:id": "~udddddddd-0000-0000-0000-000000000003",
"~:name": "Page 3"
}
},
"~:id": "~uaaaaaaaa-0000-0000-0000-000000000001",
"~:options": {
"~:components-v2": true
},
"~:recent-colors": []
}
}

View File

@ -0,0 +1,127 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
// UUIDs matching the fixture file `workspace/get-file-rename-page.json`
const FILE_ID = "aaaaaaaa-0000-0000-0000-000000000001";
const PAGE1_ID = "bbbbbbbb-0000-0000-0000-000000000001"; // non-empty (has a rectangle)
const PAGE2_ID = "cccccccc-0000-0000-0000-000000000002"; // empty
const PAGE3_ID = "dddddddd-0000-0000-0000-000000000003"; // empty
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
/**
* Returns a locator for the interactive body of a page item in the sitemap
* sidebar, identified by its page UUID.
*/
function getPageItem(page, pageId) {
return page.getByTestId(`page-${pageId}`);
}
/**
* Returns the visible name span inside a page item (not in edit mode).
*/
function getPageNameSpan(page, pageId) {
return getPageItem(page, pageId).getByTestId("page-name");
}
/**
* Double-clicks a page item to enter rename mode, types the new name and
* confirms with Enter.
*/
async function renamePage(page, pageId, newName) {
const item = getPageItem(page, pageId);
await item.dblclick();
const input = item.locator("input");
await expect(input).toBeVisible();
await input.selectText();
await input.fill(newName);
await page.keyboard.press("Enter");
}
async function setupWorkspace(workspacePage) {
await workspacePage.setupEmptyFile();
// Override the file mock with the 3-page fixture.
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-rename-page.json",
);
// Mock update-file so rename commits don't fail.
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
// The base WorkspacePage uses `this.pageName` (getByTestId("page-name")) as a
// readiness signal inside goToWorkspace. With 3 pages in the sidebar there are
// 3 matching elements, which violates Playwright's strict mode. Narrow the
// locator to the first occurrence so the internal readiness check can pass.
workspacePage.pageName = workspacePage.page
.getByTestId("page-name")
.first();
await workspacePage.goToWorkspace({
fileId: FILE_ID,
pageId: PAGE1_ID,
pageName: "Page 1",
});
}
test("User renames a non-empty page to '---' — page is renamed, URL does not change", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await setupWorkspace(workspacePage);
// Confirm we are on Page 1.
await expect(getPageNameSpan(page, PAGE1_ID)).toHaveText("Page 1");
const urlBefore = page.url();
expect(urlBefore).toContain(`page-id=${PAGE1_ID}`);
// Rename the non-empty page to "---".
await renamePage(page, PAGE1_ID, "---");
// The page name should have changed to "---".
await expect(getPageNameSpan(page, PAGE1_ID)).toHaveText("---");
// The URL must still point to the same page (no navigation triggered).
await expect(page).toHaveURL(new RegExp(`page-id=${PAGE1_ID}`));
// The page must NOT be rendered as a separator (it still has shapes).
await expect(
getPageItem(page, PAGE1_ID).getByTestId("page-separator"),
).not.toBeVisible();
});
test("User renames an empty page to '---' — page becomes a separator and URL changes", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await setupWorkspace(workspacePage);
// Navigate to the second page (empty) by clicking it in the sitemap.
const page2Item = getPageItem(page, PAGE2_ID);
await page2Item.click();
// Wait until the URL reflects the navigation to Page 2.
await expect(page).toHaveURL(new RegExp(`page-id=${PAGE2_ID}`));
// Rename the empty page to "---".
await renamePage(page, PAGE2_ID, "---");
// Since the renamed page is empty, it must now be shown as a separator.
await expect(
getPageItem(page, PAGE2_ID).getByTestId("page-separator"),
).toBeVisible();
// The application must have navigated away from the separator page to
// a different page (Page 1 or Page 3 depending on fallback logic).
await expect(page).not.toHaveURL(new RegExp(`page-id=${PAGE2_ID}`));
// The current URL must still be a workspace URL pointing to the same file.
await expect(page).toHaveURL(new RegExp(`file-id=${FILE_ID}`));
});

View File

@ -329,6 +329,9 @@
ptk/WatchEvent
(watch [it state _]
(let [page (dsh/lookup-page state id)
objects (:objects page)
empty-page? (and (= 1 (count objects))
(= uuid/zero (first (keys objects))))
changes (-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/mod-page page {:name name}))
@ -342,7 +345,11 @@
separator? (= "---" (str/trim name))]
(rx/concat
(rx/of (dch/commit-changes changes))
;; Go to other page only if page is empty (only has the root shape)
;; and the separator page is being renamed, otherwise user can rename
;; any page to separator and be forced to go to another page
(when (and separator?
empty-page?
(= id (:current-page-id state))
(some? fallback-page-id))
(rx/of (dcm/go-to-workspace :page-id fallback-page-id))))))))