Add playwright E2E tests (#2174)

This commit is contained in:
Sam Freund
2025-12-04 22:25:48 -06:00
committed by GitHub
parent f821657d2b
commit 017b074eae
18 changed files with 421 additions and 46 deletions

View File

@@ -1,8 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
# Editor directories and files
.idea
components.d.ts

View File

@@ -8,7 +8,7 @@ export default defineConfigWithVueTs(
vueTsConfigs.recommended,
skipFormattingConfig,
{
ignores: ["**/dist/**"]
ignores: ["**/dist/**", "playwright-report"]
},
{
//extends: ["js/recommended"],

View File

@@ -9,9 +9,13 @@
"build": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write src/",
"format": "prettier --write src/ tests/",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format-ci": "prettier --check src/"
"format-ci": "prettier --check src/",
"test": "playwright test",
"test-ui": "playwright test --ui",
"test-setup": "playwright install --with-deps"
},
"dependencies": {
"@fontsource/prompt": "^5.2.6",
@@ -28,6 +32,7 @@
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@playwright/test": "^1.56.1",
"@types/node": "^22.15.14",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",

View File

@@ -0,0 +1,83 @@
import { defineConfig, devices } from "@playwright/test";
import path from "path";
/**
* 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({
globalSetup: "./tests/global-setup",
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: false,
/* 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: 1,
/* 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:5800",
/* 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"] }
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
{
name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" }
}
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: process.platform == "win32" ? "" : "./" + "gradlew run",
url: "http://localhost:5800",
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,
cwd: path.normalize("../")
}
});

View File

@@ -45,6 +45,9 @@ importers:
'@eslint/js':
specifier: ^9.31.0
version: 9.31.0
'@playwright/test':
specifier: ^1.56.1
version: 1.56.1
'@types/node':
specifier: ^22.15.14
version: 22.15.14
@@ -430,6 +433,11 @@ packages:
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@playwright/test@1.56.1':
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
engines: {node: '>=18'}
hasBin: true
'@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
@@ -1019,6 +1027,11 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1247,6 +1260,16 @@ packages:
typescript:
optional: true
playwright-core@1.56.1:
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.56.1:
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
engines: {node: '>=18'}
hasBin: true
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
@@ -1759,6 +1782,10 @@ snapshots:
'@pkgr/core@0.2.4': {}
'@playwright/test@1.56.1':
dependencies:
playwright: 1.56.1
'@rolldown/pluginutils@1.0.0-beta.19': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
@@ -2399,6 +2426,9 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -2605,6 +2635,14 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
playwright-core@1.56.1: {}
playwright@1.56.1:
dependencies:
playwright-core: 1.56.1
optionalDependencies:
fsevents: 2.3.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0

View File

@@ -224,6 +224,7 @@ const handleBulkImport = () => {
v-model="importVersion"
variant="underlined"
label="Model Version"
data-testid="import-version-select"
:items="
useSettingsStore().general.supportedBackends?.includes('RKNN')
? ['YOLOv5', 'YOLOv8', 'YOLO11']
@@ -324,7 +325,7 @@ const handleBulkImport = () => {
<th>Info</th>
</tr>
</thead>
<tbody>
<tbody data-testid="model-table">
<tr v-for="model in supportedModels" :key="model.modelPath">
<td>{{ model.nickname }}</td>
<td>{{ model.labels.join(", ") }}</td>

View File

@@ -0,0 +1,16 @@
import { test as base } from "@playwright/test";
import axios from "axios";
export const test = base.extend({
page: async ({ page }, use) => {
// Use the page in the test (no per-test backend reset here)
axios.defaults.baseURL = "http://localhost:5800/api/test";
await use(page);
}
});
test.beforeAll(async () => {
console.log("Running before all tests: Resetting backend state...");
await axios.post("http://localhost:5800/api/test/resetBackend");
await axios.post("http://localhost:5800/api/test/activateTestMode");
});

View File

@@ -0,0 +1,7 @@
async function globalSetup() {
// You can perform global setup tasks here, such as starting a server or setting environment variables
const path = await import("path");
process.env.TESTS_DIR = path.resolve(process.cwd());
}
export default globalSetup;

View File

@@ -0,0 +1,79 @@
import { expect } from "@playwright/test";
import { test } from "../fixtures";
import axios from "axios";
import path from "path";
const fakeModelName = "FAKE-MODEL";
const fakeLabels = "test, 1, woof";
const newModelName = "foo-bar";
const platforms = ["LINUX_RK3588_64", "LINUX_QCS6490"];
for (const platform of platforms) {
test.describe(`Platform: ${platform}`, () => {
test.beforeEach(async ({ page }) => {
await page.goto("/#/settings");
await axios.post("/override/platform", { platform: platform });
await page.reload();
});
test("testSettingsPage", async ({ page }) => {
if (platform.endsWith("RK3588_64")) {
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with RK3588");
} else if (platform.endsWith("QCS6490")) {
await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with QCS6490");
}
await expect(page.getByText("Object Detection")).toBeVisible();
});
test("Upload model", async ({ page }) => {
const testsDir = process.env.TESTS_DIR;
if (!testsDir) {
throw new Error("TESTS_DIR is not set");
}
await page.getByRole("button", { name: "Import Model" }).click();
await page.getByRole("textbox", { name: "Labels" }).fill(fakeLabels);
await page.getByRole("spinbutton", { name: "Width" }).fill("640");
await page.getByRole("spinbutton", { name: "Height" }).fill("640");
await page.getByTestId("import-version-select").click();
await page.getByRole("option", { name: "YOLOv8" }).click();
const modelFile = platform.endsWith("RK3588_64") ? `${fakeModelName}.rknn` : `${fakeModelName}.tflite`;
await page
.getByRole("button", { name: "Model File Model File" })
.setInputFiles(path.join(testsDir, "tests/resources", modelFile));
await page.getByRole("button", { name: "Import Object Detection Model" }).click();
await page.goto("/#/settings");
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
await expect(tableRow).toBeVisible();
await expect(tableRow).toContainText(fakeLabels);
});
test("Rename model", async ({ page }) => {
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
await tableRow.getByRole("button", { name: "Rename Model" }).click();
await page.getByRole("textbox", { name: "New Name New Name" }).fill(newModelName);
await page.getByRole("button", { name: "Rename", exact: true }).click();
await page.reload();
const renamedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await expect(renamedRow).toContainText(fakeLabels);
});
test("Delete model", async ({ page }) => {
const tableRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await tableRow.getByRole("button", { name: "Delete Model" }).click();
await page.getByRole("button", { name: "Delete model", exact: true }).click();
await page.reload();
const deletedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
await expect(deletedRow).toHaveCount(0);
});
});
}