Skip to content

Commit

Permalink
Merge pull request writer#294 from raaymax/parallel-e2e-tests
Browse files Browse the repository at this point in the history
tests: parallel e2e tests runner
  • Loading branch information
ramedina86 authored Mar 11, 2024
2 parents d4ab53e + 35992b2 commit 6ab5a05
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 111 deletions.
3 changes: 2 additions & 1 deletion alfred/npm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
149 changes: 99 additions & 50 deletions e2e_tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,35 @@ 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) {
this.process.kill();
}
const ss = spawn(
"streamsync",
["edit", "./runtime", "--port", this.port]
["edit", this.path, "--port", this.port]
);
this.process = ss;
const startupTimeout = setTimeout(() => {
Expand Down Expand Up @@ -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");
Expand All @@ -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 });
})();

Expand All @@ -119,30 +143,55 @@ 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
console.log("Server is running on port 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);
}
});

4 changes: 2 additions & 2 deletions e2e_tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions e2e_tests/tests/button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
56 changes: 10 additions & 46 deletions e2e_tests/tests/components.spec.ts
Original file line number Diff line number Diff line change
@@ -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` },
Expand All @@ -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` },
Expand All @@ -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 }) => {
Expand Down
49 changes: 49 additions & 0 deletions e2e_tests/tests/componentsBasic.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

Loading

0 comments on commit 6ab5a05

Please sign in to comment.