chore: speed up e2e tests (#17262)
This commit is contained in:
parent
576cd71a44
commit
9780bd3cc7
24 changed files with 43 additions and 74 deletions
|
|
@ -6,7 +6,7 @@ test.use({ baseURL: baseUrl() });
|
|||
const BASIC = "basics.evcc.yaml";
|
||||
|
||||
test("set initial password", async ({ page }) => {
|
||||
await start(BASIC);
|
||||
await start(BASIC, null, "");
|
||||
await page.goto("/");
|
||||
|
||||
const modal = page.getByTestId("password-modal");
|
||||
|
|
@ -34,7 +34,7 @@ test("set initial password", async ({ page }) => {
|
|||
});
|
||||
|
||||
test("login", async ({ page }) => {
|
||||
await start(BASIC, "password.sql");
|
||||
await start(BASIC, "password.sql", "");
|
||||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
|
|
@ -61,7 +61,7 @@ test("login", async ({ page }) => {
|
|||
});
|
||||
|
||||
test("http iframe hint", async ({ page }) => {
|
||||
await start(BASIC, "password.sql");
|
||||
await start(BASIC, "password.sql", "");
|
||||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
|
|
@ -89,7 +89,7 @@ test("http iframe hint", async ({ page }) => {
|
|||
});
|
||||
|
||||
test("update password", async ({ page }) => {
|
||||
await start(BASIC, "password.sql");
|
||||
await start(BASIC, "password.sql", "");
|
||||
await page.goto("/");
|
||||
|
||||
const oldPassword = "secret";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "password.sql");
|
||||
await start("basics.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { enableExperimental } from "./utils";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("battery-settings.evcc.yaml", "password.sql");
|
||||
await start("battery-settings.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { enableExperimental } from "./utils";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("battery-settings.evcc.yaml", "password.sql");
|
||||
await start("battery-settings.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ test.use({ baseURL: baseUrl() });
|
|||
|
||||
test.beforeAll(async () => {
|
||||
await startSimulator();
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
@ -25,7 +25,6 @@ test.describe("battery meter", async () => {
|
|||
await page.getByRole("button", { name: "Apply changes" }).click();
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
|
|
@ -73,7 +72,6 @@ test.describe("battery meter", async () => {
|
|||
|
||||
test("advanced fields", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_EMPTY = "config-empty.evcc.yaml";
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ test.use({ baseURL: baseUrl() });
|
|||
|
||||
test.beforeAll(async () => {
|
||||
await startSimulator();
|
||||
await start(CONFIG_EMPTY, "password.sql");
|
||||
await start(CONFIG_EMPTY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
@ -32,7 +32,6 @@ test.describe("grid meter", async () => {
|
|||
await page.getByRole("button", { name: "Apply changes" }).click();
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("grid")).toHaveCount(1);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
|
|
@ -14,13 +14,12 @@ const SELECT_ALL = "ControlOrMeta+KeyA";
|
|||
|
||||
async function goToConfig(page) {
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("messaging", async () => {
|
||||
test("save a comment", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG = "config-grid-only.evcc.yaml";
|
||||
|
||||
|
|
@ -8,9 +8,8 @@ test.use({ baseURL: baseUrl() });
|
|||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await start(CONFIG, "password.sql");
|
||||
await start(CONFIG);
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
|
||||
|
||||
|
|
@ -15,13 +16,12 @@ const SELECT_ALL = "ControlOrMeta+KeyA";
|
|||
|
||||
async function goToConfig(page) {
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("tariffs", async () => {
|
||||
test("tariffs not configured", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toBeVisible();
|
||||
|
|
@ -31,7 +31,7 @@ test.describe("tariffs", async () => {
|
|||
});
|
||||
|
||||
test("tariffs via ui", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("tariffs").getByRole("button", { name: "edit" }).click();
|
||||
|
|
@ -89,7 +89,7 @@ test.describe("tariffs", async () => {
|
|||
});
|
||||
|
||||
test("tariffs from evcc.yaml", async ({ page }) => {
|
||||
await start(CONFIG_WITH_TARIFFS, "password.sql");
|
||||
await start(CONFIG_WITH_TARIFFS);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml";
|
||||
|
|
@ -14,10 +14,9 @@ test.afterEach(async () => {
|
|||
|
||||
test.describe("vehicles", async () => {
|
||||
test("create, edit and delete vehicles", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("vehicle")).toHaveCount(0);
|
||||
|
|
@ -65,10 +64,9 @@ test.describe("vehicles", async () => {
|
|||
});
|
||||
|
||||
test("config should survive restart", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("vehicle")).toHaveCount(0);
|
||||
|
|
@ -99,10 +97,9 @@ test.describe("vehicles", async () => {
|
|||
});
|
||||
|
||||
test("mixed config (yaml + db)", async ({ page }) => {
|
||||
await start(CONFIG_WITH_VEHICLE, "password.sql");
|
||||
await start(CONFIG_WITH_VEHICLE);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("vehicle")).toHaveCount(1);
|
||||
|
|
@ -120,10 +117,9 @@ test.describe("vehicles", async () => {
|
|||
});
|
||||
|
||||
test("advanced fields", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await page.getByTestId("add-vehicle").click();
|
||||
|
|
@ -159,10 +155,9 @@ test.describe("vehicles", async () => {
|
|||
});
|
||||
|
||||
test("save and restore rfid identifiers", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await page.getByTestId("add-vehicle").click();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental, login } from "./utils";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY, "password.sql");
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
@ -18,12 +18,10 @@ test.describe("basics", async () => {
|
|||
await page.goto("/");
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await login(page);
|
||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||
});
|
||||
test.skip("alert box should always be visible", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
await expect(page.getByRole("alert")).toBeVisible();
|
||||
});
|
||||
|
|
@ -37,7 +35,6 @@ test.describe("general", async () => {
|
|||
|
||||
// change value in config
|
||||
await page.goto("/#/config");
|
||||
await login(page);
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("generalconfig-title")).toContainText("Hello World");
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ function dbPath() {
|
|||
return path.join(os.tmpdir(), file);
|
||||
}
|
||||
|
||||
export async function start(config, sqlDumps, flags) {
|
||||
export async function start(config, sqlDumps, flags = "--disable-auth") {
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
|
|
@ -55,9 +55,9 @@ export async function stop(instance) {
|
|||
await _clean();
|
||||
}
|
||||
|
||||
export async function restart(config) {
|
||||
export async function restart(config, flags = "--disable-auth") {
|
||||
await _stop();
|
||||
await _start(config);
|
||||
await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function cleanRestart(config, sqlDumps) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("heating.evcc.yaml", "password.sql");
|
||||
await start("heating.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ test.afterAll(async () => {
|
|||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await start(simulatorConfig(), "password.sql");
|
||||
await start(simulatorConfig());
|
||||
|
||||
await page.goto(simulatorUrl());
|
||||
await page.getByLabel("Grid Power").fill("500");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { login } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "password.sql");
|
||||
await start("basics.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
@ -16,7 +15,6 @@ test.describe("opening logs", async () => {
|
|||
await page.goto("/");
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await login(page);
|
||||
await page.getByRole("link", { name: "Logs" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||
});
|
||||
|
|
@ -25,7 +23,6 @@ test.describe("opening logs", async () => {
|
|||
await page.evaluate(() => window.app.raise({ message: "Fake Error" }));
|
||||
await page.getByTestId("notification-icon").click();
|
||||
await page.getByRole("link", { name: "View full logs" }).click();
|
||||
await login(page);
|
||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||
});
|
||||
test("via need help", async ({ page }) => {
|
||||
|
|
@ -33,7 +30,6 @@ test.describe("opening logs", async () => {
|
|||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("button", { name: "Need Help?" }).click();
|
||||
await page.getByRole("link", { name: "View logs" }).click();
|
||||
await login(page);
|
||||
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -41,7 +37,6 @@ test.describe("opening logs", async () => {
|
|||
test.describe("features", async () => {
|
||||
test("content", async ({ page }) => {
|
||||
await page.goto("/#/log");
|
||||
await login(page);
|
||||
await page.getByTestId("log-search").fill("listening at");
|
||||
await expect(page.getByTestId("log-content")).toContainText("listening at");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorConfig } from "./simulator";
|
||||
import { login } from "./utils";
|
||||
|
||||
const BASICS_CONFIG = "basics.evcc.yaml";
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ test.use({ baseURL: baseUrl() });
|
|||
|
||||
test.describe("Basics", async () => {
|
||||
test.beforeAll(async () => {
|
||||
await start(BASICS_CONFIG, "password.sql");
|
||||
await start(BASICS_CONFIG);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
|
@ -21,9 +20,6 @@ test.describe("Basics", async () => {
|
|||
test("Menu options. No battery and grid.", async ({ page }) => {
|
||||
for (const route of UI_ROUTES) {
|
||||
await page.goto(route);
|
||||
if (route === "/#/config") {
|
||||
await login(page);
|
||||
}
|
||||
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||
|
|
@ -53,7 +49,7 @@ test.describe("Basics", async () => {
|
|||
test.describe("Advanced", async () => {
|
||||
test.beforeAll(async () => {
|
||||
await startSimulator();
|
||||
await start(simulatorConfig(), "password.sql");
|
||||
await start(simulatorConfig());
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
|
@ -64,9 +60,6 @@ test.describe("Advanced", async () => {
|
|||
test("Menu options. All available.", async ({ page }) => {
|
||||
for (const route of UI_ROUTES) {
|
||||
await page.goto(route);
|
||||
if (route === "/#/config") {
|
||||
await login(page);
|
||||
}
|
||||
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ test.describe.configure({ mode: "parallel" });
|
|||
const CONFIG = "plan.evcc.yaml";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await start(CONFIG, "password.sql");
|
||||
await start(CONFIG);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const mobile = devices["iPhone 12 Mini"].viewport;
|
|||
const desktop = devices["Desktop Chrome"].viewport;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", ["password.sql", "sessions.sql"]);
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("smart-cost-only.evcc.yaml", "password.sql");
|
||||
await start("smart-cost-only.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ test.use({ baseURL: baseUrl() });
|
|||
|
||||
test.beforeAll(async () => {
|
||||
await startSimulator();
|
||||
await start(simulatorConfig(), "password.sql");
|
||||
await start(simulatorConfig());
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("statistics.evcc.yaml", ["password.sql", "statistics.sql"]);
|
||||
await start("statistics.evcc.yaml", "statistics.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,3 @@ export async function enableExperimental(page) {
|
|||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(page.locator(".modal-backdrop")).not.toBeVisible();
|
||||
}
|
||||
|
||||
export async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("vehicle-error.evcc.yaml", "password.sql");
|
||||
await start("vehicle-error.evcc.yaml");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ test.afterAll(async () => {
|
|||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await start(simulatorConfig(), "password.sql");
|
||||
await start(simulatorConfig());
|
||||
|
||||
await page.goto(simulatorUrl());
|
||||
await page.getByLabel("Grid Power").fill("500");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue