diff --git a/alfred/npm.py b/alfred/npm.py index 3dcf85411..743f6da70 100644 --- a/alfred/npm.py +++ b/alfred/npm.py @@ -8,7 +8,8 @@ def npm_lint(): @alfred.command("npm.e2e", help="run e2e tests") @alfred.option('--browser', '-b', help="run e2e tests on specified browser", default='chromium') def npm_test(browser): - alfred.run("npm run e2e:"+browser+":ci") + with alfred.env(CI="true"): + alfred.run("npm run e2e:"+browser+":ci") @alfred.command("npm.build", help="build ui code") def npm_build(): diff --git a/e2e_tests/index.js b/e2e_tests/index.js index 518be9f97..4fb1f527b 100644 --- a/e2e_tests/index.js +++ b/e2e_tests/index.js @@ -3,14 +3,27 @@ const fs = require("node:fs").promises; const { spawn } = require("node:child_process"); const httpProxy = require("http-proxy"); -class Streamsync { - constructor() { +function* port(port) { + while (true) { + yield port++; + } +} + +function* id() { + let id = 1; + while (true) { + yield id++; + } +} + +class StreamsyncProcess { + constructor(path, port) { + this.path = path; this.process = null; this.initialized = false; - this.port = 7358; + this.port = port; this.busy = false; - } - + } async start() { return new Promise((resolve, reject) => { if (this.process !== null) { @@ -18,7 +31,7 @@ class Streamsync { } const ss = spawn( "streamsync", - ["edit", "./runtime", "--port", this.port] + ["edit", this.path, "--port", this.port] ); this.process = ss; const startupTimeout = setTimeout(() => { @@ -63,10 +76,19 @@ class Streamsync { }); } + get pid() { + return this.process.pid; + } + async stop() { return new Promise((resolve) => { if (this.process) { + const timeout = setTimeout(() => { + console.warn("Killing process", this.process.pid); + this.process.kill("SIGKILL"); + }, 15000); this.process.once("exit", () => { + clearTimeout(timeout); resolve(); }); this.process.kill("SIGTERM"); @@ -75,38 +97,40 @@ class Streamsync { } }); } +} - async restart() { - this.busy = true; - try { - await this.stop(); - this.port += 1; - await this.start(); - } catch (e) { - throw e; - } finally { - this.busy = false; - } - } +class StreamsyncProcessPool { + constructor() { + this.genPort = port(7358); + this.genId = id(); + this.processes = {}; + } + + async start(preset) { + const id = this.genId.next().value; + await fs.mkdir(`./runtime/${id}`); + await fs.copyFile(`./presets/${preset}/ui.json`, `./runtime/${id}/ui.json`); + await fs.copyFile(`./presets/${preset}/main.py`, `./runtime/${id}/main.py`); + const process = new StreamsyncProcess(`./runtime/${id}`, this.genPort.next().value); + await process.start(); + this.processes[id] = process; + return id; + } + + async stop(id) { + const process = this.processes[id]; + if(process) { + await process.stop(); + delete this.processes[id]; + } + await fs.rm(`./runtime/${id}`, { recursive: true }); + } - async loadPreset(preset) { - this.busy = true; - try { - await this.stop(); - this.port += 1; - await fs.copyFile(`./presets/${preset}/ui.json`, "./runtime/ui.json"); - await fs.copyFile(`./presets/${preset}/main.py`, "./runtime/main.py"); - await this.start(); - } catch (e) { - throw e; - } finally { - this.busy = false; - } - } } -const ss = new Streamsync(); +const sspp = new StreamsyncProcessPool(); (async () => { + await fs.rm(`./runtime`, { recursive: true, force: true }); await fs.mkdir("runtime", { recursive: true }); })(); @@ -119,24 +143,41 @@ proxy.on('error', function (e) { const app = express(); -app.get("/preset/:preset", async (req, res) => { - if(ss.busy) { - res.status(429).send("Server is busy"); - return; - } - console.log("Loading preset", req.params.preset); - const preset = req.params.preset; - await ss.loadPreset(preset); - res.send("UI updated"); +app.post("/preset/:preset", async (req, res) => { + try { + console.log("Loading preset", req.params.preset); + const id = await sspp.start(req.params.preset); + res.json({url: `/${id}/`}) + } catch (e) { + console.error(e); + res.status(500).send(e); + } +}); + +app.delete("/:id/", async (req, res) => { + try { + await sspp.stop(req.params.id); + res.send("Server cleanup"); + } catch (e) { + console.error(e); + res.status(500).send(e); + } +}); + +app.use('/:id/', (req, res) => { + try { + const process = sspp.processes[req.params.id]; + if(!process || process.initialized === false) { + res.send("Server not initialized yet"); + return; + } + proxy.web(req, res, {target: 'http://127.0.0.1:'+ process.port}); + } catch (e) { + console.error(e); + res.status(500).send(e); + } }); -app.use((req, res) => { - if(ss.initialized === false) { - res.send("Server not initialized yet"); - return; - } - proxy.web(req, res, {target: 'http://127.0.0.1:'+ ss.port}); -}) const server = app.listen(7357, () => { // eslint-disable-next-line no-console @@ -144,5 +185,13 @@ const server = app.listen(7357, () => { }); server.on('upgrade', (req, socket, head) => { - proxy.ws(req, socket, head, {target: 'ws://127.0.0.1:'+ss.port, ws: true}); + try{ + const id = req.url.split("/")[1]; + const ss = sspp.processes[id]; + req.url = req.url.replace(`/${id}/`, '/'); + proxy.ws(req, socket, head, {target: 'ws://127.0.0.1:'+ss.port, ws: true}); + } catch (e) { + console.error(e); + } }); + diff --git a/e2e_tests/playwright.config.ts b/e2e_tests/playwright.config.ts index 4d7202880..bce46f17b 100644 --- a/e2e_tests/playwright.config.ts +++ b/e2e_tests/playwright.config.ts @@ -4,8 +4,8 @@ export default defineConfig({ testDir: "./tests", fullyParallel: false, forbidOnly: !!process.env.CI, - retries: 2, - workers: 1, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, reporter: "list", use: { baseURL: "http://127.0.0.1:7357", diff --git a/e2e_tests/tests/button.spec.ts b/e2e_tests/tests/button.spec.ts index 8cdf27f20..db0e06448 100644 --- a/e2e_tests/tests/button.spec.ts +++ b/e2e_tests/tests/button.spec.ts @@ -3,14 +3,20 @@ import { test, expect } from "@playwright/test"; test.describe("button", () => { const TYPE = "button"; const COMPONENT_LOCATOR = `button.CoreButton.component`; + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/section`); + const response = await request.post(`/preset/section`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); test.setTimeout(5000); }); diff --git a/e2e_tests/tests/components.spec.ts b/e2e_tests/tests/components.spec.ts index 39f8f377f..f255fb559 100644 --- a/e2e_tests/tests/components.spec.ts +++ b/e2e_tests/tests/components.spec.ts @@ -1,13 +1,5 @@ import { test, expect } from "@playwright/test"; -test.setTimeout(5000); - -const createAndRemove = [ - { type: "sidebar", locator: `div.CoreSidebar.component` }, - { type: "section", locator: `section.CoreSection.component` }, - { type: "columns", locator: `div.CoreColumns.component` }, -]; - const fullCheck = [ { type: "button", locator: `button.CoreButton.component` }, { type: "text", locator: `div.CoreText.component` }, @@ -28,9 +20,9 @@ const fullCheck = [ { type: "googlemaps", locator: `div.CoreGoogleMaps.component` }, { type: "icon", locator: `div.icon.component` }, { type: "timer", locator: `div.CoreTimer.component` }, - { type: "textinput", locator: `div.CoreTextInput.component` }, + { type: "textinput", locator: `div.CoreTextInput.component label` }, { type: "textareainput", locator: `div.CoreTextareaInput.component` }, - { type: "numberinput", locator: `div.CoreNumberInput.component` }, + { type: "numberinput", locator: `div.CoreNumberInput.component label` }, { type: "sliderinput", locator: `div.CoreSliderInput.component label` }, { type: "dateinput", locator: `div.CoreDateInput.component label` }, { type: "radioinput", locator: `div.CoreRadioInput.component` }, @@ -51,54 +43,26 @@ const fullCheck = [ { type: "ratinginput", locator: `div.CoreRatingInput.component` }, ]; -createAndRemove.forEach(({ type, locator }) => { - test.describe(type, () => { - const TYPE = type; - const COMPONENT_LOCATOR = locator; - const TARGET = ".CorePage"; - - test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/empty_page`); - expect(response.ok()).toBeTruthy(); - }); - - test.beforeEach(async ({ page }) => { - await page.goto("/"); - }); - - test("create and remove", async ({ page }) => { - await page - .locator(`div.component.button[data-component-type="${TYPE}"]`) - .dragTo(page.locator(TARGET)); - await expect( - page.locator(TARGET + " " + COMPONENT_LOCATOR), - ).toHaveCount(1); - - await page.locator(COMPONENT_LOCATOR).click(); - await page - .locator( - '.BuilderComponentShortcuts .actionButton[data-automation-action="delete"]', - ) - .click(); - await expect(page.locator(COMPONENT_LOCATOR)).toHaveCount(0); - }); - }); -}); - fullCheck.forEach(({ type, locator }) => { test.describe(type, () => { const TYPE = type; const COMPONENT_LOCATOR = locator; const COLUMN1 = ".CoreColumns .CoreColumn:nth-child(1 of .CoreColumn)"; const COLUMN2 = ".CoreColumns .CoreColumn:nth-child(2 of .CoreColumn)"; + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/2columns`); + const response = await request.post(`/preset/2columns`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); }); test("create, drag and drop and remove", async ({ page }) => { diff --git a/e2e_tests/tests/componentsBasic.spec.ts b/e2e_tests/tests/componentsBasic.spec.ts new file mode 100644 index 000000000..677f54202 --- /dev/null +++ b/e2e_tests/tests/componentsBasic.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "@playwright/test"; + +const createAndRemove = [ + { type: "sidebar", locator: `div.CoreSidebar.component` }, + { type: "section", locator: `section.CoreSection.component` }, + { type: "columns", locator: `div.CoreColumns.component` }, +]; + + +createAndRemove.forEach(({ type, locator }) => { + test.describe(type, () => { + const TYPE = type; + const COMPONENT_LOCATOR = locator; + const TARGET = ".CorePage"; + let url: string; + + test.beforeAll(async ({request}) => { + const response = await request.post(`/preset/empty_page`); + expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); + }); + + test.beforeEach(async ({ page }) => { + await page.goto(url); + }); + + test("create and remove", async ({ page }) => { + await page + .locator(`div.component.button[data-component-type="${TYPE}"]`) + .dragTo(page.locator(TARGET)); + await expect( + page.locator(TARGET + " " + COMPONENT_LOCATOR), + ).toHaveCount(1); + + await page.locator(COMPONENT_LOCATOR).click(); + await page + .locator( + '.BuilderComponentShortcuts .actionButton[data-automation-action="delete"]', + ) + .click(); + await expect(page.locator(COMPONENT_LOCATOR)).toHaveCount(0); + }); + }); +}); + diff --git a/e2e_tests/tests/drag.spec.ts b/e2e_tests/tests/drag.spec.ts index 0af74d95b..760ee1004 100644 --- a/e2e_tests/tests/drag.spec.ts +++ b/e2e_tests/tests/drag.spec.ts @@ -3,14 +3,20 @@ import { test, expect } from "@playwright/test"; test.describe("drag", () => { const COMPONENT_LOCATOR = `section.CoreSection.component`; const COLUMN = ".CoreColumns .CoreColumn:nth-child(1 of .CoreColumn)"; + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/2columns`); + const response = await request.post(`/preset/2columns`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); }); test("drag and drop component into itself", async ({ page }) => { diff --git a/e2e_tests/tests/image.spec.ts b/e2e_tests/tests/image.spec.ts index 3bdfe9885..dbb8aa1c4 100644 --- a/e2e_tests/tests/image.spec.ts +++ b/e2e_tests/tests/image.spec.ts @@ -3,14 +3,20 @@ import { test, expect } from "@playwright/test"; test.describe("image", () => { const TYPE = "image"; const COMPONENT_LOCATOR = `div.CoreImage.component`; + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/section`); + const response = await request.post(`/preset/section`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); }); test("configure", async ({ page }) => { diff --git a/e2e_tests/tests/state.spec.ts b/e2e_tests/tests/state.spec.ts index 3954db7dc..b00a2359a 100644 --- a/e2e_tests/tests/state.spec.ts +++ b/e2e_tests/tests/state.spec.ts @@ -23,13 +23,19 @@ const execute = async (page) => { } test.describe("state", () => { + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/state`); + const response = await request.post(`/preset/state`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); }); test("increment number", async ({ page }) => { diff --git a/e2e_tests/tests/undoRedo.spec.ts b/e2e_tests/tests/undoRedo.spec.ts index a12174071..4ef88e6c4 100644 --- a/e2e_tests/tests/undoRedo.spec.ts +++ b/e2e_tests/tests/undoRedo.spec.ts @@ -1,20 +1,24 @@ import { test, expect } from "@playwright/test"; -test.setTimeout(5000); - test.describe('undo and redo', () => { const TYPE = 'button'; const COMPONENT_LOCATOR = 'button.CoreButton.component'; const COLUMN1 = ".CoreColumns .CoreColumn:nth-child(1 of .CoreColumn)"; const COLUMN2 = ".CoreColumns .CoreColumn:nth-child(2 of .CoreColumn)"; + let url: string; test.beforeAll(async ({request}) => { - const response = await request.get(`/preset/2columns`); + const response = await request.post(`/preset/2columns`); expect(response.ok()).toBeTruthy(); + ({url} = await response.json()); + }); + + test.afterAll(async ({request}) => { + await request.delete(url); }); test.beforeEach(async ({ page }) => { - await page.goto("/"); + await page.goto(url); }); test("create, drag and drop, property change and remove", async ({ page }) => {