mirror of
https://github.com/penpot/penpot.git
synced 2026-09-07 20:49:37 +00:00
🐛 Fix rename on non empty page (#9850)
This commit is contained in:
parent
017f1d9994
commit
9439d63682
308
frontend/playwright/data/workspace/get-file-rename-page.json
Normal file
308
frontend/playwright/data/workspace/get-file-rename-page.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
127
frontend/playwright/ui/specs/rename-page.spec.js
Normal file
127
frontend/playwright/ui/specs/rename-page.spec.js
Normal 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}`));
|
||||||
|
});
|
||||||
@ -329,6 +329,9 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [it state _]
|
(watch [it state _]
|
||||||
(let [page (dsh/lookup-page state id)
|
(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)
|
changes (-> (pcb/empty-changes it)
|
||||||
(pcb/with-page page)
|
(pcb/with-page page)
|
||||||
(pcb/mod-page page {:name name}))
|
(pcb/mod-page page {:name name}))
|
||||||
@ -342,7 +345,11 @@
|
|||||||
separator? (= "---" (str/trim name))]
|
separator? (= "---" (str/trim name))]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of (dch/commit-changes changes))
|
(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?
|
(when (and separator?
|
||||||
|
empty-page?
|
||||||
(= id (:current-page-id state))
|
(= id (:current-page-id state))
|
||||||
(some? fallback-page-id))
|
(some? fallback-page-id))
|
||||||
(rx/of (dcm/go-to-workspace :page-id fallback-page-id))))))))
|
(rx/of (dcm/go-to-workspace :page-id fallback-page-id))))))))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user