From d56a574d27dbab654633fe967c6ad43fb58be696 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Thu, 15 Jan 2026 09:08:28 +0100 Subject: [PATCH 01/13] setup playwright --- .github/workflows/playwright.yml | 36 +++++++++++ frontend/webEditor/.gitignore | 9 ++- frontend/webEditor/package-lock.json | 82 ++++++++++++++++++++++++ frontend/webEditor/package.json | 5 +- frontend/webEditor/playwright.config.ts | 63 ++++++++++++++++++ frontend/webEditor/tests/example.spec.ts | 18 ++++++ 6 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 frontend/webEditor/playwright.config.ts create mode 100644 frontend/webEditor/tests/example.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..7f07a204 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,36 @@ +name: Playwright Tests +on: + workflow_dispatch: + push: + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies and build + working-directory: frontend/webEditor + run: | + npm install + npm run build + + - name: Install Playwright Browsers + working-directory: frontend/webEditor + run: npx playwright install --with-deps + + - name: Run Playwright tests + working-directory: frontend/webEditor + run: npx run test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/frontend/webEditor/.gitignore b/frontend/webEditor/.gitignore index 81071fd2..67387d5e 100644 --- a/frontend/webEditor/.gitignore +++ b/frontend/webEditor/.gitignore @@ -1,3 +1,10 @@ node_modules dist -src/helpUi/hash.json \ No newline at end of file +src/helpUi/hash.json + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/frontend/webEditor/package-lock.json b/frontend/webEditor/package-lock.json index 188fdc17..2a9de36b 100644 --- a/frontend/webEditor/package-lock.json +++ b/frontend/webEditor/package-lock.json @@ -11,6 +11,8 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@fortawesome/fontawesome-free": "^7.0.0", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.8", "@vscode/codicons": "^0.0.44", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -704,6 +706,22 @@ "reflect-metadata": "0.2.2" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", @@ -1026,6 +1044,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", @@ -2470,6 +2498,53 @@ "node": ">=0.10" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2954,6 +3029,13 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/frontend/webEditor/package.json b/frontend/webEditor/package.json index b14b9cb9..18c7ec1f 100644 --- a/frontend/webEditor/package.json +++ b/frontend/webEditor/package.json @@ -11,6 +11,8 @@ "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@fortawesome/fontawesome-free": "^7.0.0", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.8", "@vscode/codicons": "^0.0.44", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -37,7 +39,8 @@ "postprepare": "npm run fetch-hash", "prebuild": "npm run fetch-hash", "predev": "npm run fetch-hash", - "fetch-hash": "node ./scripts/fetchHash.js" + "fetch-hash": "node ./scripts/fetchHash.js", + "test": "playwright test" }, "lint-staged": { "*.{html,css,ts,tsx,json}": [ diff --git a/frontend/webEditor/playwright.config.ts b/frontend/webEditor/playwright.config.ts new file mode 100644 index 00000000..7c94aeef --- /dev/null +++ b/frontend/webEditor/playwright.config.ts @@ -0,0 +1,63 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: "http://localhost:4173", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + expect: { + toMatchSnapshot: { + maxDiffPixelRatio: 0.01, + }, + }, + /* Run local dev server before starting the tests */ + webServer: { + command: "vite preview", + port: 4173, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/frontend/webEditor/tests/example.spec.ts b/frontend/webEditor/tests/example.spec.ts new file mode 100644 index 00000000..a149fe32 --- /dev/null +++ b/frontend/webEditor/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("https://playwright.dev/"); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/"); + + // Click the get started link. + await page.getByRole("link", { name: "Get started" }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible(); +}); From 9a2a3fde116f071cda5b56c78f287a4fa2e45ee1 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Fri, 16 Jan 2026 12:27:48 +0100 Subject: [PATCH 02/13] test delete, copy and paste --- frontend/webEditor/playwright.config.ts | 1 + .../webEditor/tests/commandPalette.spec.ts | 0 frontend/webEditor/tests/creationTool.spec.ts | 0 frontend/webEditor/tests/example.spec.ts | 18 --------- frontend/webEditor/tests/shortcuts.spec.ts | 38 ++++++++++++++++++ frontend/webEditor/tests/utils.ts | 39 +++++++++++++++++++ 6 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 frontend/webEditor/tests/commandPalette.spec.ts create mode 100644 frontend/webEditor/tests/creationTool.spec.ts delete mode 100644 frontend/webEditor/tests/example.spec.ts create mode 100644 frontend/webEditor/tests/shortcuts.spec.ts create mode 100644 frontend/webEditor/tests/utils.ts diff --git a/frontend/webEditor/playwright.config.ts b/frontend/webEditor/playwright.config.ts index 7c94aeef..f9586158 100644 --- a/frontend/webEditor/playwright.config.ts +++ b/frontend/webEditor/playwright.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", + // headless: false }, /* Configure projects for major browsers */ diff --git a/frontend/webEditor/tests/commandPalette.spec.ts b/frontend/webEditor/tests/commandPalette.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/webEditor/tests/creationTool.spec.ts b/frontend/webEditor/tests/creationTool.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/webEditor/tests/example.spec.ts b/frontend/webEditor/tests/example.spec.ts deleted file mode 100644 index a149fe32..00000000 --- a/frontend/webEditor/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("has title", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test("get started link", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Click the get started link. - await page.getByRole("link", { name: "Get started" }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible(); -}); diff --git a/frontend/webEditor/tests/shortcuts.spec.ts b/frontend/webEditor/tests/shortcuts.spec.ts new file mode 100644 index 00000000..fb7d4cd9 --- /dev/null +++ b/frontend/webEditor/tests/shortcuts.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test"; +import { + getControlKeyEquivalent, + init, + isPresent, + pressKey, + selectById, + takeGraphScreenshot, + waitForElement, +} from "./utils"; + +test("delte, undo, redo", async ({ page, browserName }) => { + const ID = "sprotty_8j2r1g"; + const HTML_ID = "#" + ID; + const CONTROL_KEY = getControlKeyEquivalent(browserName); + await init(page); + expect(await isPresent(page, HTML_ID)).toBeTruthy(); + const originalScreenshot = await takeGraphScreenshot(page); + + await selectById(page, HTML_ID); + await pressKey(page, "Delete"); + await waitForElement(page, HTML_ID, false); + expect(await isPresent(page, HTML_ID)).toBeFalsy(); + const removedScreenshot = await takeGraphScreenshot(page); + expect(removedScreenshot).not.toEqual(originalScreenshot); + + await pressKey(page, CONTROL_KEY, "Z"); + await waitForElement(page, HTML_ID, true); + expect(await isPresent(page, HTML_ID)).toBeTruthy(); + const undoScreenshot = await takeGraphScreenshot(page); + expect(undoScreenshot).not.toEqual(removedScreenshot); + + await pressKey(page, CONTROL_KEY, "Shift", "Z"); + await waitForElement(page, HTML_ID, false); + expect(await isPresent(page, HTML_ID)).toBeFalsy(); + const redoScreenshot = await takeGraphScreenshot(page); + expect(redoScreenshot).not.toEqual(undoScreenshot); +}); diff --git a/frontend/webEditor/tests/utils.ts b/frontend/webEditor/tests/utils.ts new file mode 100644 index 00000000..0e4e8ee4 --- /dev/null +++ b/frontend/webEditor/tests/utils.ts @@ -0,0 +1,39 @@ +import { Page } from "@playwright/test"; + +export async function takeGraphScreenshot(page: Page) { + const graphs = await page.locator(".sprotty-graph").all(); + if (graphs.length < 1) { + throw "No graph element found"; + } + if (graphs.length > 1) { + throw "Multiple graph elements found"; + } + return graphs[0].screenshot(); +} + +export async function selectById(page: Page, id: string) { + await page.locator(id).click(); +} + +export async function pressKey(page: Page, ...keys: string[]) { + await page.keyboard.press(keys.join("+")); +} + +export function getControlKeyEquivalent(browserName: "chromium" | "firefox" | "webkit") { + return browserName === "webkit" ? "Meta" : "Control"; +} + +export async function waitForElement(page: Page, id: string, present = true) { + const options = present ? undefined : { state: "detached" as const }; + // @ts-expect-error This should work. Just giving it undefined does... + return page.waitForSelector(id, options); +} + +export async function isPresent(page: Page, id: string) { + return (await page.locator(id).count()) > 0; +} + +export async function init(page: Page) { + await page.goto("/"); + await page.waitForSelector(".sprotty-graph"); +} From e525ac14d79d19ac37eaec99fe7459c1b9f801eb Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Fri, 16 Jan 2026 12:53:46 +0100 Subject: [PATCH 03/13] test layout --- frontend/webEditor/tests/shortcuts.spec.ts | 37 ++++++++++++++++------ frontend/webEditor/tests/utils.ts | 14 ++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/frontend/webEditor/tests/shortcuts.spec.ts b/frontend/webEditor/tests/shortcuts.spec.ts index fb7d4cd9..11761904 100644 --- a/frontend/webEditor/tests/shortcuts.spec.ts +++ b/frontend/webEditor/tests/shortcuts.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from "@playwright/test"; import { getControlKeyEquivalent, + getPosition, init, isPresent, pressKey, @@ -10,29 +11,45 @@ import { } from "./utils"; test("delte, undo, redo", async ({ page, browserName }) => { - const ID = "sprotty_8j2r1g"; - const HTML_ID = "#" + ID; + const ID = "#sprotty_8j2r1g"; const CONTROL_KEY = getControlKeyEquivalent(browserName); await init(page); - expect(await isPresent(page, HTML_ID)).toBeTruthy(); + expect(await isPresent(page, ID)).toBeTruthy(); const originalScreenshot = await takeGraphScreenshot(page); - await selectById(page, HTML_ID); + await selectById(page, ID); await pressKey(page, "Delete"); - await waitForElement(page, HTML_ID, false); - expect(await isPresent(page, HTML_ID)).toBeFalsy(); + await waitForElement(page, ID, false); + expect(await isPresent(page, ID)).toBeFalsy(); const removedScreenshot = await takeGraphScreenshot(page); expect(removedScreenshot).not.toEqual(originalScreenshot); await pressKey(page, CONTROL_KEY, "Z"); - await waitForElement(page, HTML_ID, true); - expect(await isPresent(page, HTML_ID)).toBeTruthy(); + await waitForElement(page, ID, true); + expect(await isPresent(page, ID)).toBeTruthy(); const undoScreenshot = await takeGraphScreenshot(page); expect(undoScreenshot).not.toEqual(removedScreenshot); await pressKey(page, CONTROL_KEY, "Shift", "Z"); - await waitForElement(page, HTML_ID, false); - expect(await isPresent(page, HTML_ID)).toBeFalsy(); + await waitForElement(page, ID, false); + expect(await isPresent(page, ID)).toBeFalsy(); const redoScreenshot = await takeGraphScreenshot(page); expect(redoScreenshot).not.toEqual(undoScreenshot); }); + +test("layout", async ({ page, browserName }) => { + const ID = "#sprotty_4myuyr"; + const CONTROL_KEY = getControlKeyEquivalent(browserName); + await init(page); + const originalScreenshot = await takeGraphScreenshot(page); + const originalPostion = await getPosition(page, ID); + + await pressKey(page, CONTROL_KEY, "L"); + await page.waitForTimeout(1000); + + const newScreenshot = await takeGraphScreenshot(page); + const newPosition = await getPosition(page, ID); + + expect(newPosition).not.toEqual(originalPostion); + expect(newScreenshot).not.toEqual(originalScreenshot); +}); diff --git a/frontend/webEditor/tests/utils.ts b/frontend/webEditor/tests/utils.ts index 0e4e8ee4..1782de83 100644 --- a/frontend/webEditor/tests/utils.ts +++ b/frontend/webEditor/tests/utils.ts @@ -36,4 +36,18 @@ export async function isPresent(page: Page, id: string) { export async function init(page: Page) { await page.goto("/"); await page.waitForSelector(".sprotty-graph"); + await page.focus(".sprotty-graph"); +} + +export async function getPosition(page: Page, id: string) { + const element = await page.locator(id).first(); + if ((await element.count()) == 0) { + throw "Element not found"; + } + const transform = (await element.getAttribute("transform")) ?? ""; + const match = /translate\((\d+(?:\.\d+)?), *(\d+(?:\.\d+)?)\)/.exec(transform); + if (!match) { + return { x: 0, y: 0 }; + } + return { x: Number(match[1]), y: Number(match[2]) }; } From 917b14ec63024539799269bc75cfd5dd5d5cdf28 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sun, 18 Jan 2026 08:27:23 +0100 Subject: [PATCH 04/13] add browser types --- frontend/webEditor/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/webEditor/tsconfig.json b/frontend/webEditor/tsconfig.json index ae41ac20..1b8ad401 100644 --- a/frontend/webEditor/tsconfig.json +++ b/frontend/webEditor/tsconfig.json @@ -6,6 +6,7 @@ "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client"], "skipLibCheck": true, // Bundler mode From 95f4f7add8d8a6a9655816dbb4e053e0a9c5878b Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Sun, 18 Jan 2026 10:36:25 +0100 Subject: [PATCH 05/13] add initial command palette tests --- frontend/webEditor/playwright.config.ts | 14 +- frontend/webEditor/tests/README.md | 2 + .../webEditor/tests/commandPalette.spec.ts | 124 ++++++++++++++++++ frontend/webEditor/tests/utils.ts | 8 +- 4 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 frontend/webEditor/tests/README.md diff --git a/frontend/webEditor/playwright.config.ts b/frontend/webEditor/playwright.config.ts index f9586158..4418038a 100644 --- a/frontend/webEditor/playwright.config.ts +++ b/frontend/webEditor/playwright.config.ts @@ -30,25 +30,23 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", - // headless: false + headless: false, }, /* Configure projects for major browsers */ projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, - + /*{ + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, { name: "webkit", use: { ...devices["Desktop Safari"] }, - }, + },*/ ], expect: { toMatchSnapshot: { diff --git a/frontend/webEditor/tests/README.md b/frontend/webEditor/tests/README.md new file mode 100644 index 00000000..f0ce841b --- /dev/null +++ b/frontend/webEditor/tests/README.md @@ -0,0 +1,2 @@ +## WebKit +While most shortcuts use Meta key instead of Control, for opening the command palette Control needs to used, as Meta+Space is something else on MacOs and thus it would conflict. (idk its wierd) \ No newline at end of file diff --git a/frontend/webEditor/tests/commandPalette.spec.ts b/frontend/webEditor/tests/commandPalette.spec.ts index e69de29b..09091c79 100644 --- a/frontend/webEditor/tests/commandPalette.spec.ts +++ b/frontend/webEditor/tests/commandPalette.spec.ts @@ -0,0 +1,124 @@ +import test, { expect, Page } from "@playwright/test"; +import { init, pressKey } from "./utils"; + +const COMMAND_PALETTE_ID = "#sprotty_command-palette"; + +test("Test filter working", async ({ page }) => { + const LOAD = "Load"; + const SAVE = "Save"; + const LOAD_DEFAULT = "Load default diagram"; + const FIT = "Fit to Screen"; + const LAYOUT = "Layout diagram"; + + await init(page); + await openPalette(page); + await expectNames([LOAD, SAVE, LOAD_DEFAULT, FIT, LAYOUT], { + [LOAD]: ["JSON", "DFD and DD", "Palladio"], + [SAVE]: ["JSON", "DFD and DD"], + [LAYOUT]: ["Lines", "Wrapping Lines", "Circles"], + }); + + const input = page.locator(COMMAND_PALETTE_ID + " > input"); + + // test filter by parent category. should be case insensitive + await input.fill("Load"); + await expectNames([LOAD, LOAD_DEFAULT], { [LOAD]: ["JSON", "DFD and DD", "Palladio"] }); + await input.fill("load"); + await expectNames([LOAD, LOAD_DEFAULT], { [LOAD]: ["JSON", "DFD and DD", "Palladio"] }); + + // test filter by child category. should be case insensitive + await input.fill("JSON"); + await expectNames([LOAD, SAVE], { [LOAD]: ["JSON"], [SAVE]: ["JSON"] }); + await input.fill("json"); + await expectNames([LOAD, SAVE], { [LOAD]: ["JSON"], [SAVE]: ["JSON"] }); + + // test for something that appears in both (LAyout, PalLAdio) + await input.fill("LA"); + await expectNames([LOAD, LAYOUT], { [LOAD]: ["Palladio"], [LAYOUT]: ["Lines", "Wrapping Lines", "Circles"] }); + + async function expectNames(names: string[], children?: Record) { + const suggestions = page.locator(COMMAND_PALETTE_ID + " .command-palette-suggestions-holder > *"); + expect(await suggestions.count(), names.join(", ")).toEqual(names.length); + + for (let i = 0; i < names.length; i++) { + const suggestionName = await suggestions + .nth(i) + .locator(":scope > .command-palette-suggestion-label") + .innerText(); + expect(suggestionName).toContain(names[i]); + + if (children !== undefined && children[names[i]] !== undefined) { + const expectedChildren = children[names[i]]!; + const foundChildren = suggestions.nth(i).locator(":scope > .command-palette-suggestion-children > *"); + expect(await foundChildren.count(), `Children of: ${names[i]}; ${expectedChildren.join(", ")}`).toEqual( + expectedChildren.length, + ); + + for (let j = 0; j < expectedChildren.length; j++) { + const childName = await foundChildren + .nth(j) + .locator(":scope > .command-palette-suggestion-label") + .innerText(); + expect(childName).toContain(expectedChildren[j]); + } + } + } + } +}); + +// skipped due to bug when no file (will need files eventually, but no file should still be tested) +test.skip("Test load", async ({ page }) => { + await init(page); + + await testWithFileChooser(0, ["json"], false); + await testWithFileChooser(1, ["dataflowdiagram", "datadictionary"], true); + await testWithFileChooser( + 2, + [ + "pddc", + "allocation", + "allocation", + "nodecharacteristics", + "repository", + "resourceenvironment", + "system", + "usagemodel", + ], + true, + ); + + async function testWithFileChooser(childIndex: number, fileTypes: string[], multiple: boolean) { + await openPalette(page); + const [fileChooser] = await Promise.all([page.waitForEvent("filechooser"), select(page, 0, childIndex)]); + await select(page, 0, 0); + const acceptedTypes = (await fileChooser.element().getAttribute("accept")) ?? ""; + expect(fileChooser.isMultiple()).toBe(multiple); + expect(acceptedTypes.split(",").length, `Found: ${acceptedTypes}, Expected: ${fileTypes.join(", ")}`).toBe( + fileTypes.length, + ); + for (const fileType of fileTypes) { + expect(acceptedTypes).toContain(fileType); + } + fileChooser.setFiles([]); + } +}); + +async function openPalette(page: Page) { + await pressKey(page, "Control", "Space"); + await page.waitForSelector(COMMAND_PALETTE_ID, { state: "visible" }); +} + +async function select(page: Page, parentIndex: number, childIndex?: number) { + // as we start with no selection we have to press down one extra time + for (let i = 0; i < parentIndex + 1; i++) { + await pressKey(page, "ArrowDown"); + } + if (childIndex !== undefined) { + await pressKey(page, "ArrowRight"); + for (let i = 0; i < childIndex; i++) { + await pressKey(page, "ArrowDown"); + } + } + await page.waitForTimeout(500); + await pressKey(page, "Enter"); +} diff --git a/frontend/webEditor/tests/utils.ts b/frontend/webEditor/tests/utils.ts index 1782de83..bdafa755 100644 --- a/frontend/webEditor/tests/utils.ts +++ b/frontend/webEditor/tests/utils.ts @@ -40,7 +40,7 @@ export async function init(page: Page) { } export async function getPosition(page: Page, id: string) { - const element = await page.locator(id).first(); + const element = page.locator(id).first(); if ((await element.count()) == 0) { throw "Element not found"; } @@ -51,3 +51,9 @@ export async function getPosition(page: Page, id: string) { } return { x: Number(match[1]), y: Number(match[2]) }; } + +export async function mockBackEnd(page: Page, route: string, response: string) { + await page.route(`*/**/${route}`, async (route) => { + await route.fulfill({ body: response }); + }); +} From 06abbc14bd2b43598010f4f9611b751d7ca0c318 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sun, 18 Jan 2026 13:42:45 +0100 Subject: [PATCH 06/13] test creation tool --- frontend/webEditor/playwright.config.ts | 8 +-- frontend/webEditor/tests/creationTool.spec.ts | 55 +++++++++++++++++++ frontend/webEditor/tests/utils.ts | 4 +- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/frontend/webEditor/playwright.config.ts b/frontend/webEditor/playwright.config.ts index 4418038a..2d76ce33 100644 --- a/frontend/webEditor/playwright.config.ts +++ b/frontend/webEditor/playwright.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", - headless: false, + //headless: false, }, /* Configure projects for major browsers */ @@ -39,14 +39,14 @@ export default defineConfig({ name: "firefox", use: { ...devices["Desktop Firefox"] }, }, - /*{ + { name: "chromium", use: { ...devices["Desktop Chrome"] }, - }, + }, { name: "webkit", use: { ...devices["Desktop Safari"] }, - },*/ + }, ], expect: { toMatchSnapshot: { diff --git a/frontend/webEditor/tests/creationTool.spec.ts b/frontend/webEditor/tests/creationTool.spec.ts index e69de29b..41e3d834 100644 --- a/frontend/webEditor/tests/creationTool.spec.ts +++ b/frontend/webEditor/tests/creationTool.spec.ts @@ -0,0 +1,55 @@ +import test, { expect } from "@playwright/test"; +import { getControlKeyEquivalent, init, pressKey, waitForElement } from "./utils"; + +test.only("test creation tools", async ({ page, browserName }) => { + const CONTROL_KEY = getControlKeyEquivalent(browserName); + await init(page); + + // clear diagram + await pressKey(page, CONTROL_KEY, "A"); + await waitForElement(page, ".sprotty-node.selected"); + await pressKey(page, "Delete"); + await waitForElement(page, ".sprotty-node", false); + expect(await page.locator(".sprotty-node").count()).toBe(0); + expect(await page.locator(".sprotty-port").count()).toBe(0); + expect(await page.locator(".sprotty-edge").count()).toBe(0); + + const toolPalette = page.locator("#sprotty_tool-palette"); + + const storageNode = await placeNode(0, "storage"); + const ioNode = await placeNode(1, "input-output"); + await placeNode(2, "function"); + + const inputPort = await placePort(4, "#" + storageNode); + const outputPort = await placePort(5, "#" + ioNode); + + await clickToolPalette(3); + await page.click("#" + outputPort); + await page.click("#" + inputPort); + await waitForElement(page, ".sprotty-edge"); + expect(await page.locator(".sprotty-edge").count()).toBe(1); + + function clickToolPalette(index: number) { + return toolPalette.locator(":scope > .tool").nth(index).click(); + } + + async function placeNode(index: number, type: string) { + await clickToolPalette(index); + await page.click("#sprotty_root", { position: { x: 100, y: 100 + index * 100 } }); + const selector = `.sprotty-node.${type}`; + await waitForElement(page, selector); + const newNode = page.locator(selector); + expect(await newNode.count()).toBe(1); + return (await newNode.getAttribute("id"))!; + } + + async function placePort(index: number, node: string) { + await clickToolPalette(index); + await page.click(node, { position: { x: 10, y: 10 } }); + const selector = `${node} > .sprotty-port`; + await waitForElement(page, selector); + const newPort = page.locator(selector); + expect(await newPort.count()).toBe(1); + return (await newPort.getAttribute("id"))!; + } +}); diff --git a/frontend/webEditor/tests/utils.ts b/frontend/webEditor/tests/utils.ts index bdafa755..f304c54b 100644 --- a/frontend/webEditor/tests/utils.ts +++ b/frontend/webEditor/tests/utils.ts @@ -23,10 +23,10 @@ export function getControlKeyEquivalent(browserName: "chromium" | "firefox" | "w return browserName === "webkit" ? "Meta" : "Control"; } -export async function waitForElement(page: Page, id: string, present = true) { +export async function waitForElement(page: Page, selector: string, present = true) { const options = present ? undefined : { state: "detached" as const }; // @ts-expect-error This should work. Just giving it undefined does... - return page.waitForSelector(id, options); + return page.waitForSelector(selector, options); } export async function isPresent(page: Page, id: string) { From 977e3a4b8552bc4cfad71972952006f51597fecc Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 19 Jan 2026 08:32:47 +0100 Subject: [PATCH 07/13] fix workflow --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7f07a204..fd6414c9 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -26,7 +26,7 @@ jobs: - name: Run Playwright tests working-directory: frontend/webEditor - run: npx run test + run: npm run test - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} From e812bfdfb1696b133dd19f1ffa6cf97521a89461 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 19 Jan 2026 08:37:44 +0100 Subject: [PATCH 08/13] test creation tool --- frontend/webEditor/tests/creationTool.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/webEditor/tests/creationTool.spec.ts b/frontend/webEditor/tests/creationTool.spec.ts index 41e3d834..d66e2f86 100644 --- a/frontend/webEditor/tests/creationTool.spec.ts +++ b/frontend/webEditor/tests/creationTool.spec.ts @@ -1,7 +1,7 @@ import test, { expect } from "@playwright/test"; import { getControlKeyEquivalent, init, pressKey, waitForElement } from "./utils"; -test.only("test creation tools", async ({ page, browserName }) => { +test("test creation tools", async ({ page, browserName }) => { const CONTROL_KEY = getControlKeyEquivalent(browserName); await init(page); From b43378f6d4930c6cc63fcd73f88fac92e8e04056 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 19 Jan 2026 12:07:51 +0100 Subject: [PATCH 09/13] test saving files over command palette --- .../webEditor/tests/assets/dfd.datadictionary | 15 +++++ .../tests/assets/dfd.dataflowdiagram | 6 ++ frontend/webEditor/tests/assets/dfd.json | 30 +++++++++ frontend/webEditor/tests/assets/json.json | 30 +++++++++ frontend/webEditor/tests/assets/palladio.json | 30 +++++++++ .../webEditor/tests/commandPalette.spec.ts | 61 ++++++++++++++++++- frontend/webEditor/tests/mockWebSocket.ts | 48 +++++++++++++++ frontend/webEditor/tests/utils.ts | 21 +++++-- 8 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 frontend/webEditor/tests/assets/dfd.datadictionary create mode 100644 frontend/webEditor/tests/assets/dfd.dataflowdiagram create mode 100644 frontend/webEditor/tests/assets/dfd.json create mode 100644 frontend/webEditor/tests/assets/json.json create mode 100644 frontend/webEditor/tests/assets/palladio.json create mode 100644 frontend/webEditor/tests/mockWebSocket.ts diff --git a/frontend/webEditor/tests/assets/dfd.datadictionary b/frontend/webEditor/tests/assets/dfd.datadictionary new file mode 100644 index 00000000..32695c2a --- /dev/null +++ b/frontend/webEditor/tests/assets/dfd.datadictionary @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/webEditor/tests/assets/dfd.dataflowdiagram b/frontend/webEditor/tests/assets/dfd.dataflowdiagram new file mode 100644 index 00000000..6934333e --- /dev/null +++ b/frontend/webEditor/tests/assets/dfd.dataflowdiagram @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/webEditor/tests/assets/dfd.json b/frontend/webEditor/tests/assets/dfd.json new file mode 100644 index 00000000..873e3ce8 --- /dev/null +++ b/frontend/webEditor/tests/assets/dfd.json @@ -0,0 +1,30 @@ +{ + "canvasBounds": { "x": 0, "y": 0, "width": 1549, "height": 919 }, + "scroll": { "x": -718.5708429602527, "y": -356.4018869430463 }, + "zoom": 0.8135132245021809, + "position": { "x": 0, "y": 0 }, + "size": { "width": -1, "height": -1 }, + "features": {}, + "type": "graph", + "id": "root", + "children": [ + { + "position": { "x": 299.5, "y": 119 }, + "size": { "width": -1, "height": -1 }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "dfd", + "labels": [], + "ports": [], + "hideLabels": false, + "minimumWidth": 50, + "annotations": [], + "features": {}, + "id": "1cdyzc", + "type": "node:input-output", + "children": [] + } + ] +} diff --git a/frontend/webEditor/tests/assets/json.json b/frontend/webEditor/tests/assets/json.json new file mode 100644 index 00000000..5de07e50 --- /dev/null +++ b/frontend/webEditor/tests/assets/json.json @@ -0,0 +1,30 @@ +{ + "canvasBounds": { "x": 0, "y": 0, "width": 1549, "height": 919 }, + "scroll": { "x": -718.5708429602527, "y": -356.4018869430463 }, + "zoom": 0.8135132245021809, + "position": { "x": 0, "y": 0 }, + "size": { "width": -1, "height": -1 }, + "features": {}, + "type": "graph", + "id": "root", + "children": [ + { + "position": { "x": 299.5, "y": 119 }, + "size": { "width": -1, "height": -1 }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "json", + "labels": [], + "ports": [], + "hideLabels": false, + "minimumWidth": 50, + "annotations": [], + "features": {}, + "id": "1cdyzc", + "type": "node:input-output", + "children": [] + } + ] +} diff --git a/frontend/webEditor/tests/assets/palladio.json b/frontend/webEditor/tests/assets/palladio.json new file mode 100644 index 00000000..838ce698 --- /dev/null +++ b/frontend/webEditor/tests/assets/palladio.json @@ -0,0 +1,30 @@ +{ + "canvasBounds": { "x": 0, "y": 0, "width": 1549, "height": 919 }, + "scroll": { "x": -718.5708429602527, "y": -356.4018869430463 }, + "zoom": 0.8135132245021809, + "position": { "x": 0, "y": 0 }, + "size": { "width": -1, "height": -1 }, + "features": {}, + "type": "graph", + "id": "root", + "children": [ + { + "position": { "x": 299.5, "y": 119 }, + "size": { "width": -1, "height": -1 }, + "strokeWidth": 0, + "selected": false, + "hoverFeedback": false, + "opacity": 1, + "text": "palladio", + "labels": [], + "ports": [], + "hideLabels": false, + "minimumWidth": 50, + "annotations": [], + "features": {}, + "id": "1cdyzc", + "type": "node:input-output", + "children": [] + } + ] +} diff --git a/frontend/webEditor/tests/commandPalette.spec.ts b/frontend/webEditor/tests/commandPalette.spec.ts index 09091c79..b5bd9cd3 100644 --- a/frontend/webEditor/tests/commandPalette.spec.ts +++ b/frontend/webEditor/tests/commandPalette.spec.ts @@ -1,5 +1,5 @@ import test, { expect, Page } from "@playwright/test"; -import { init, pressKey } from "./utils"; +import { getZoom, init, pressKey, takeGraphScreenshot, focus } from "./utils"; const COMMAND_PALETTE_ID = "#sprotty_command-palette"; @@ -103,6 +103,65 @@ test.skip("Test load", async ({ page }) => { } }); +// flaky +test("Test save", async ({ page, browserName }) => { + await init(page); + await openPalette(page); + + const [jsonDownload] = await Promise.all([page.waitForEvent("download"), select(page, 1, 0)]); + const jsonFileName = jsonDownload.suggestedFilename(); + expect(jsonFileName.endsWith(".json"), `Expected ${jsonFileName} to end with .json`).toBeTruthy(); + await jsonDownload.delete(); + + // as webkit has issues downloading multiple files, we skip for now + if (browserName === "webkit") { + return; + } + + // due to browser animations focusing the downloads section late, we need to wait for them to finish here + await page.waitForTimeout(1000); + await focus(page); + await openPalette(page); + + const [dfdDownload, ddDownload] = await Promise.all([ + page.waitForEvent("download", (d) => d.suggestedFilename().includes("dataflowdiagram")), + page.waitForEvent("download", (d) => d.suggestedFilename().includes("datadictionary")), + select(page, 1, 1), + ]); + const dfdFileName = dfdDownload.suggestedFilename(); + const ddFileName = ddDownload.suggestedFilename(); + + expect( + dfdFileName.endsWith(".dataflowdiagram"), + `Expected ${dfdFileName} to end with .dataflowdiagram`, + ).toBeTruthy(); + expect(ddFileName.endsWith(".datadictionary"), `Expected ${ddFileName} to end with .datadictionary`).toBeTruthy(); + await dfdDownload.delete(); + await ddDownload.delete(); +}); + +test("Test Fit to Screen", async ({ page }) => { + await init(page); + await openPalette(page); + + const initialZoom = await getZoom(page); + const initialScreenshot = await takeGraphScreenshot(page); + + await page.mouse.wheel(0, 100); + await page.waitForTimeout(500); + const postScrollZoom = await getZoom(page); + expect(postScrollZoom).not.toBe(initialZoom); + const postScrollScreenshot = await takeGraphScreenshot(page); + expect(postScrollScreenshot).not.toEqual(initialScreenshot); + + await select(page, 4); + await page.waitForTimeout(500); + const postFitZoom = await getZoom(page); + expect(postFitZoom).not.toBe(postScrollZoom); + const postFitScreenshot = await takeGraphScreenshot(page); + expect(postFitScreenshot).not.toEqual(postScrollScreenshot); +}); + async function openPalette(page: Page) { await pressKey(page, "Control", "Space"); await page.waitForSelector(COMMAND_PALETTE_ID, { state: "visible" }); diff --git a/frontend/webEditor/tests/mockWebSocket.ts b/frontend/webEditor/tests/mockWebSocket.ts new file mode 100644 index 00000000..ab61cfb0 --- /dev/null +++ b/frontend/webEditor/tests/mockWebSocket.ts @@ -0,0 +1,48 @@ +import { readFileSync } from "node:fs"; + +export class MockWebSocket { + public static INSTANCE: MockWebSocket; + + url: string; + readyState: number = WebSocket.CONNECTING; + onopen?: () => void; + onmessage?: (ev: MessageEvent) => void; + onclose?: () => void; + onerror?: () => void; + + constructor(url: string) { + this.url = url; + MockWebSocket.INSTANCE = this; + + // simulate async connect + setTimeout(() => { + this.readyState = WebSocket.OPEN; + this.onopen?.(); + }, 0); + } + + send(data: string) { + let response: string | undefined = undefined; + if (data.startsWith("Json2DFD")) { + const dd = readFileSync("./assets/dfd.datadictionary"); + const dfd = readFileSync("./assets/dfd.dataflowdiagram"); + response = `dfd:${dfd}${dd}`; + } + + if (response) { + setTimeout(() => { + this._emitMessage(response); + }, 0); + } + } + + close() { + this.readyState = WebSocket.CLOSED; + this.onclose?.(); + } + + /** helper for tests */ + _emitMessage(data: string) { + this.onmessage?.(new MessageEvent("message", { data })); + } +} diff --git a/frontend/webEditor/tests/utils.ts b/frontend/webEditor/tests/utils.ts index f304c54b..85c186c4 100644 --- a/frontend/webEditor/tests/utils.ts +++ b/frontend/webEditor/tests/utils.ts @@ -1,4 +1,5 @@ import { Page } from "@playwright/test"; +import { MockWebSocket } from "./mockWebSocket"; export async function takeGraphScreenshot(page: Page) { const graphs = await page.locator(".sprotty-graph").all(); @@ -34,8 +35,16 @@ export async function isPresent(page: Page, id: string) { } export async function init(page: Page) { + await page.addInitScript(() => { + // @ts-expect-error not an exact match, but mocks everything we need + window.WebSocket = MockWebSocket; + }); await page.goto("/"); await page.waitForSelector(".sprotty-graph"); + await focus(page); +} + +export async function focus(page: Page) { await page.focus(".sprotty-graph"); } @@ -52,8 +61,12 @@ export async function getPosition(page: Page, id: string) { return { x: Number(match[1]), y: Number(match[2]) }; } -export async function mockBackEnd(page: Page, route: string, response: string) { - await page.route(`*/**/${route}`, async (route) => { - await route.fulfill({ body: response }); - }); +export async function getZoom(page: Page) { + const rootG = page.locator("#sprotty_root > g"); + const transform = (await rootG.getAttribute("transform")) ?? ""; + const match = /scale\((\d+(?:\.\d+))\)/.exec(transform); + if (match) { + return Number(match[1]); + } + return NaN; } From 6d550bcf11cf48c2355ce2e82deb470649d37ed1 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 19 Jan 2026 13:18:11 +0100 Subject: [PATCH 10/13] test layout over command palette --- .../webEditor/tests/commandPalette.spec.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/frontend/webEditor/tests/commandPalette.spec.ts b/frontend/webEditor/tests/commandPalette.spec.ts index b5bd9cd3..0fe393a6 100644 --- a/frontend/webEditor/tests/commandPalette.spec.ts +++ b/frontend/webEditor/tests/commandPalette.spec.ts @@ -1,5 +1,5 @@ import test, { expect, Page } from "@playwright/test"; -import { getZoom, init, pressKey, takeGraphScreenshot, focus } from "./utils"; +import { getZoom, init, pressKey, takeGraphScreenshot, focus, getPosition } from "./utils"; const COMMAND_PALETTE_ID = "#sprotty_command-palette"; @@ -103,7 +103,6 @@ test.skip("Test load", async ({ page }) => { } }); -// flaky test("Test save", async ({ page, browserName }) => { await init(page); await openPalette(page); @@ -162,6 +161,52 @@ test("Test Fit to Screen", async ({ page }) => { expect(postFitScreenshot).not.toEqual(postScrollScreenshot); }); +test("Test layout", async ({ page }) => { + const ID = "#sprotty_4myuyr"; + await init(page); + const previousScreenshots = [await takeGraphScreenshot(page)]; + const previousPositions = [await getPosition(page, ID)]; + + // Lines + await testLayout(0); + // Wrapping Lines + await testLayout(1); + // Circles + await testLayout(2); + + // test default which should be Lines + await openPalette(page); + await select(page, 4); + await page.waitForTimeout(250); + const newScreenshot = await takeGraphScreenshot(page); + const newPosition = await getPosition(page, ID); + expect(newPosition).toEqual(previousPositions[1]); + expect(newScreenshot).not.toEqual(previousScreenshots); + for (const i of [0, 2, 3]) { + expect(newPosition).not.toEqual(previousPositions[i]); + expect(newScreenshot).not.toEqual(previousScreenshots[i]); + } + + async function testLayout(childIndex: number) { + await openPalette(page); + await select(page, 4, childIndex); + await page.waitForTimeout(250); + const newScreenshot = await takeGraphScreenshot(page); + const newPosition = await getPosition(page, ID); + for (const previousPosition of previousPositions) { + expect( + newPosition, + `Expected (${newPosition.x},${newPosition.y}) not to be (${previousPosition.x},${previousPosition.y}) at ${childIndex}`, + ).not.toEqual(previousPosition); + } + for (const previousScreenshot of previousScreenshots) { + expect(newScreenshot, `Expected screenshot difference at ${childIndex}`).not.toEqual(previousScreenshot); + } + previousPositions.push(newPosition); + previousScreenshots.push(newScreenshot); + } +}); + async function openPalette(page: Page) { await pressKey(page, "Control", "Space"); await page.waitForSelector(COMMAND_PALETTE_ID, { state: "visible" }); @@ -178,6 +223,6 @@ async function select(page: Page, parentIndex: number, childIndex?: number) { await pressKey(page, "ArrowDown"); } } - await page.waitForTimeout(500); + await page.waitForTimeout(250); await pressKey(page, "Enter"); } From bbc09b5284a91d9cf5981d1928f6d51de4b14bcc Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Tue, 20 Jan 2026 13:03:00 +0100 Subject: [PATCH 11/13] fix report upload --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index fd6414c9..301e2455 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -32,5 +32,5 @@ jobs: if: ${{ !cancelled() }} with: name: playwright-report - path: playwright-report/ + path: frontend/webEditor/playwright-report/ retention-days: 30 From df7feabf439f5d16a80b3e242633e43a8ebb5ea9 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Tue, 20 Jan 2026 18:45:25 +0100 Subject: [PATCH 12/13] force click on creation tool test --- frontend/webEditor/tests/creationTool.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/webEditor/tests/creationTool.spec.ts b/frontend/webEditor/tests/creationTool.spec.ts index d66e2f86..dd543613 100644 --- a/frontend/webEditor/tests/creationTool.spec.ts +++ b/frontend/webEditor/tests/creationTool.spec.ts @@ -24,8 +24,8 @@ test("test creation tools", async ({ page, browserName }) => { const outputPort = await placePort(5, "#" + ioNode); await clickToolPalette(3); - await page.click("#" + outputPort); - await page.click("#" + inputPort); + await page.click("#" + outputPort, { force: true }); + await page.click("#" + inputPort, { force: true }); await waitForElement(page, ".sprotty-edge"); expect(await page.locator(".sprotty-edge").count()).toBe(1); @@ -35,7 +35,7 @@ test("test creation tools", async ({ page, browserName }) => { async function placeNode(index: number, type: string) { await clickToolPalette(index); - await page.click("#sprotty_root", { position: { x: 100, y: 100 + index * 100 } }); + await page.click("#sprotty_root", { position: { x: 100, y: 100 + index * 100 }, force: true }); const selector = `.sprotty-node.${type}`; await waitForElement(page, selector); const newNode = page.locator(selector); @@ -45,7 +45,7 @@ test("test creation tools", async ({ page, browserName }) => { async function placePort(index: number, node: string) { await clickToolPalette(index); - await page.click(node, { position: { x: 10, y: 10 } }); + await page.click(node, { position: { x: 10, y: 10 }, force: true }); const selector = `${node} > .sprotty-port`; await waitForElement(page, selector); const newPort = page.locator(selector); From 39025ee88fa1da91b1b2d5efed8d0f870c932415 Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Wed, 21 Jan 2026 12:44:31 +0100 Subject: [PATCH 13/13] try hovering before --- frontend/webEditor/playwright.config.ts | 2 +- frontend/webEditor/tests/creationTool.spec.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/webEditor/playwright.config.ts b/frontend/webEditor/playwright.config.ts index 2d76ce33..ab42ce51 100644 --- a/frontend/webEditor/playwright.config.ts +++ b/frontend/webEditor/playwright.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", - //headless: false, + // headless: false, }, /* Configure projects for major browsers */ diff --git a/frontend/webEditor/tests/creationTool.spec.ts b/frontend/webEditor/tests/creationTool.spec.ts index dd543613..bf64a143 100644 --- a/frontend/webEditor/tests/creationTool.spec.ts +++ b/frontend/webEditor/tests/creationTool.spec.ts @@ -35,7 +35,7 @@ test("test creation tools", async ({ page, browserName }) => { async function placeNode(index: number, type: string) { await clickToolPalette(index); - await page.click("#sprotty_root", { position: { x: 100, y: 100 + index * 100 }, force: true }); + await page.click("#sprotty_root", { position: { x: 200, y: 100 + index * 100 }, force: true }); const selector = `.sprotty-node.${type}`; await waitForElement(page, selector); const newNode = page.locator(selector); @@ -45,6 +45,9 @@ test("test creation tools", async ({ page, browserName }) => { async function placePort(index: number, node: string) { await clickToolPalette(index); + // we hover and then move to avoid clicking the annotation ui + await page.hover(node, { position: { x: 50, y: 10 } }); + await page.waitForTimeout(750); await page.click(node, { position: { x: 10, y: 10 }, force: true }); const selector = `${node} > .sprotty-port`; await waitForElement(page, selector);