mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge pull request #6646 from penpot/ladybenko-10904-playwright-wasm
🔧 Set up visual regression tests for wasm renderer
This commit is contained in:
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -11,3 +11,4 @@ node_modules/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/**/visual-specs/**/*.png
|
||||
|
||||
|
||||
@@ -53,6 +53,21 @@ export default defineConfig({
|
||||
toHaveScreenshot: { maxDiffPixelRatio: 0.005 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "render-wasm",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
|
||||
deviceScaleFactor: 2,
|
||||
},
|
||||
testDir: "./playwright/ui/render-wasm-specs",
|
||||
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.005,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
|
||||
BIN
frontend/playwright/data/render-wasm/assets/penguins.jpg
Normal file
BIN
frontend/playwright/data/render-wasm/assets/penguins.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 964 KiB |
1047
frontend/playwright/data/render-wasm/get-file-shapes-fills.json
Normal file
1047
frontend/playwright/data/render-wasm/get-file-shapes-fills.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1210
frontend/playwright/data/render-wasm/get-file-shapes-strokes.json
Normal file
1210
frontend/playwright/data/render-wasm/get-file-shapes-strokes.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,33 @@ export class BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
static async mockAsset(page, assetId, assetFilename, options) {
|
||||
const ids = Array.isArray(assetId) ? assetId : [assetId];
|
||||
|
||||
for (const id of ids) {
|
||||
const url = `**/assets/by-file-media-id/${id}`;
|
||||
|
||||
await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
path: `playwright/data/${assetFilename}`,
|
||||
status: 200,
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js?ts=*";
|
||||
return await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/javascript",
|
||||
body: `var penpotFlags = "${flags.join(" ")}";`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#page = null;
|
||||
|
||||
constructor(page) {
|
||||
@@ -38,14 +65,11 @@ export class BasePage {
|
||||
}
|
||||
|
||||
async mockConfigFlags(flags) {
|
||||
const url = "**/js/config.js?ts=*";
|
||||
return await this.page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/javascript",
|
||||
body: `var penpotFlags = "${flags.join(" ")}";`,
|
||||
}),
|
||||
);
|
||||
return BasePage.mockConfigFlags(this.page, flags);
|
||||
}
|
||||
|
||||
async mockAsset(assetId, assetFilename, options) {
|
||||
return BasePage.mockAsset(this.page, assetId, assetFilename, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
frontend/playwright/ui/pages/WasmWorkspacePage.js
Normal file
31
frontend/playwright/ui/pages/WasmWorkspacePage.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "./WorkspacePage";
|
||||
|
||||
export class WasmWorkspacePage extends WorkspacePage {
|
||||
static async init(page) {
|
||||
await super.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, [
|
||||
"enable-feature-render-wasm",
|
||||
"enable-render-wasm-dpr",
|
||||
]);
|
||||
|
||||
await page.addInitScript(() => {
|
||||
document.addEventListener("wasm:set-objects-finished", () => {
|
||||
window.wasmSetObjectsFinished = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.canvas = page.getByTestId("canvas-wasm-shapes");
|
||||
}
|
||||
|
||||
async waitForFirstRender() {
|
||||
await expect(this.pageName).toHaveText("Page 1");
|
||||
await this.canvas.waitFor({ state: "visible" });
|
||||
await this.page.waitForFunction(() => {
|
||||
return window.wasmSetObjectsFinished;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
);
|
||||
}
|
||||
|
||||
async mockGetFile(jsonFile) {
|
||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
|
||||
71
frontend/playwright/ui/render-wasm-specs/shapes.spec.js
Normal file
71
frontend/playwright/ui/render-wasm-specs/shapes.spec.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockConfigFlags(page, [
|
||||
"enable-feature-render-wasm",
|
||||
"enable-render-wasm-dpr",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Renders a file with basic shapes, boards and groups", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-shapes-groups-boards.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "53a7ff09-2228-81d3-8006-4b5eac177245",
|
||||
pageId: "53a7ff09-2228-81d3-8006-4b5eac177246",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with solid, gradient and image fills", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockAsset(
|
||||
[
|
||||
"1ebcea38-f1bf-8101-8006-4c8fd68e7c84",
|
||||
"1ebcea38-f1bf-8101-8006-4c8f579da49c",
|
||||
],
|
||||
"render-wasm/assets/penguins.jpg",
|
||||
);
|
||||
await workspace.mockGetFile("render-wasm/get-file-shapes-fills.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "1ebcea38-f1bf-8101-8006-4c8ec4a9bffe",
|
||||
pageId: "1ebcea38-f1bf-8101-8006-4c8ec4a9bfff",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with strokes", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockAsset(
|
||||
[
|
||||
"202c1104-9385-81d3-8006-5074e4682cac",
|
||||
"202c1104-9385-81d3-8006-5074c50339b6",
|
||||
"202c1104-9385-81d3-8006-507560ce29e3",
|
||||
],
|
||||
"render-wasm/assets/penguins.jpg",
|
||||
);
|
||||
await workspace.mockGetFile("render-wasm/get-file-shapes-strokes.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "202c1104-9385-81d3-8006-507413ff2c99",
|
||||
pageId: "202c1104-9385-81d3-8006-507413ff2c9a",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -361,6 +361,7 @@
|
||||
:viewport-ref viewport-ref}])]
|
||||
|
||||
[:canvas {:id "render"
|
||||
:data-testid "canvas-wasm-shapes"
|
||||
:ref canvas-ref
|
||||
:class (stl/css :render-shapes)
|
||||
:key (dm/str "render" page-id)
|
||||
|
||||
@@ -766,13 +766,17 @@
|
||||
|
||||
(defn process-pending
|
||||
[pending]
|
||||
(when-let [pending (-> (d/index-by :key :callback pending) vals)]
|
||||
(->> (rx/from pending)
|
||||
(rx/mapcat (fn [callback] (callback)))
|
||||
(rx/reduce conj [])
|
||||
(rx/subs! (fn [_]
|
||||
(clear-drawing-cache)
|
||||
(request-render "set-objects"))))))
|
||||
(let [event (js/CustomEvent. "wasm:set-objects-finished")
|
||||
pending (-> (d/index-by :key :callback pending) vals)]
|
||||
(if (not-empty? pending)
|
||||
(->> (rx/from pending)
|
||||
(rx/mapcat (fn [callback] (callback)))
|
||||
(rx/reduce conj [])
|
||||
(rx/subs! (fn [_]
|
||||
(clear-drawing-cache)
|
||||
(request-render "set-objects")
|
||||
(.dispatchEvent ^js js/document event))))
|
||||
(.dispatchEvent ^js js/document event))))
|
||||
|
||||
(defn process-object
|
||||
[shape]
|
||||
|
||||
53
render-wasm/docs/visual_regression_tests.md
Normal file
53
render-wasm/docs/visual_regression_tests.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Visual regression tests
|
||||
|
||||
> ⚠️ At the time being, these tests are to be run _locally_. They are not
|
||||
> executed in CI yet.
|
||||
|
||||
## Run the tests
|
||||
|
||||
The tests are located in their own Playwright project, `render-wasm`. To run them, go to the `frontend` dir and execute Playwright passing the `--project` flag:
|
||||
|
||||
```zsh
|
||||
cd frontend
|
||||
npx playwright test --ui --project=render-wasm
|
||||
```
|
||||
|
||||
## Write new tests
|
||||
|
||||
You need to add a new spec file to `frontend/playwright/ui/render-wasm-specs` or add a test to one of the existing specs. You can use `shapes.spec.js` as reference.
|
||||
|
||||
Writing the tests is very similar to write any other test for Penpot. However, some helpers have been added to address some issues specific to the new render engine.
|
||||
|
||||
### Step 1: Initialize the page
|
||||
|
||||
There is a page helper, `WasmWorkspacePage` that contains:
|
||||
|
||||
- Automatically setting the right flags to enable the new wasm renderer.
|
||||
- A helper function to wait for the first render to happen, `waitForFirstRender`.
|
||||
- A locator for the `<canvas>` element in the viewport, `canvas`.
|
||||
|
||||
> :⚠️: An await for `waitForFirstRender` is crucial, specially if the render depends on requests that are run in a promise, like fetching images or fonts (even if they are mocked/intercepted!).
|
||||
|
||||
### Step 2: Intercept requests
|
||||
|
||||
The main requests of the API to intercept are: `/get-file` and `/assets/by-file-media-id/`
|
||||
|
||||
If you disable the feature flag `fdata/pointer-map` in your local environment, you will get a JSON response from `/get-file` that does _not_ include fragments, so you will not have to mock `get-file-fragment` too.
|
||||
|
||||
For mocking the assets, a new helper has been added: `mockAsset`. This accepts either an asset ID or an array of IDs.
|
||||
|
||||
### Step 3: Go to workspace and take a screenshot
|
||||
|
||||
After calling `goToWorkspace`, you need to add this to ensure that the `<canvas>` has been drawn into:
|
||||
|
||||
```js
|
||||
await workspace.waitForFirstRender();
|
||||
```
|
||||
|
||||
To take a screenshot, simply call:
|
||||
|
||||
```js
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
```
|
||||
|
||||
Note that the test will fail the very first time you call it, because it will have no screenshot to compare to. It should work on following runs.
|
||||
Reference in New Issue
Block a user