mirror of https://github.com/jetkvm/kvm.git
feat: add e2e test for OTA process
This commit is contained in:
parent
5e87d792d8
commit
64a514520c
|
|
@ -14,4 +14,11 @@ node_modules
|
|||
#internal/native/include
|
||||
#internal/native/lib
|
||||
|
||||
ui/reports
|
||||
ui/reports
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
|
|
|||
|
|
@ -22,3 +22,10 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
"@inlang/plugin-m-function-matcher": "^2.1.0",
|
||||
"@inlang/plugin-message-format": "^4.0.0",
|
||||
"@inlang/sdk": "^2.4.9",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
|
|
@ -1276,6 +1277,22 @@
|
|||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
|
||||
|
|
@ -6188,6 +6205,53 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"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/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.12",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"@playwright/test": "^1.56.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
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: 1,
|
||||
timeout: 10 * 60 * 1000, // 10 minutes
|
||||
/* 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:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on',
|
||||
video: 'on',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
// Headless must be disabled otherwise WebRTC will not work properly
|
||||
use: { ...devices['Desktop Chrome'], headless: false },
|
||||
},
|
||||
|
||||
// {
|
||||
// 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: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@ export default function StatusCard({
|
|||
) : null}
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-semibold leading-none transition text-ellipsis">
|
||||
<div className="text-xs font-semibold leading-none transition text-ellipsis" role="status">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-xs leading-none">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Page } from "@playwright/test";
|
||||
|
||||
import { m } from "../localization/paraglide/messages.js";
|
||||
|
||||
type I18nKey = keyof typeof m;
|
||||
type I18nOptions = Record<string, string>;
|
||||
|
||||
export const translate = (i18nKey: I18nKey, options?: I18nOptions) => (
|
||||
m[i18nKey as keyof typeof m] as (options?: I18nOptions) => string
|
||||
)(options);
|
||||
|
||||
export const getByText = (page: Page, i18nKey: I18nKey, options?: I18nOptions) => (
|
||||
page.getByText(translate(i18nKey, options))
|
||||
);
|
||||
|
||||
export const getByRole = (page: Page, role: Parameters<typeof page.getByRole>[0], i18nKey: I18nKey) => (
|
||||
page.getByRole(role, { name: translate(i18nKey) })
|
||||
);
|
||||
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { m } from '../localization/paraglide/messages';
|
||||
|
||||
import { getByRole, getByText } from './helper';
|
||||
|
||||
const TARGET_DEVICE_IP = "192.168.2.4"
|
||||
|
||||
test('upgrade process', async ({ page }) => {
|
||||
await page.goto(`http://${TARGET_DEVICE_IP}`);
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/JetKVM/);
|
||||
|
||||
// Except ICE gathering completed or JetKVM device connected
|
||||
await expect(getByText(page, 'ice_gathering_completed').or(getByText(page, 'peer_connection_connected'))).toBeVisible();
|
||||
|
||||
// Except No HDMI signal detected, as the device is not connected to the HDMI port
|
||||
await expect(getByText(page, 'video_overlay_no_hdmi_signal')).toBeVisible();
|
||||
|
||||
// Emulate upgrade process
|
||||
await getByRole(page, 'button', 'action_bar_settings').click();
|
||||
await expect(getByText(page, 'general_page_description')).toBeVisible();
|
||||
|
||||
const btnCheckForUpdates = getByRole(page, 'button', 'general_check_for_updates');
|
||||
await expect(btnCheckForUpdates).toBeVisible();
|
||||
await btnCheckForUpdates.click();
|
||||
|
||||
// if System is to update, we'll then skip the update process and go to the next step
|
||||
if (await getByText(page, 'general_update_up_to_date_title').isVisible()) {
|
||||
test.skip(true, 'System is up to date, skipping update process');
|
||||
return;
|
||||
}
|
||||
|
||||
// LoadingState -> Checking for updates...
|
||||
await expect(getByText(page, 'general_update_checking_title')).toBeVisible();
|
||||
|
||||
// UpdateAvailableState -> Prompt to update the device
|
||||
await expect(getByText(page, 'general_update_available_title')).toBeVisible();
|
||||
const btnUpdateNow = getByRole(page, 'button', 'general_update_now_button');
|
||||
await expect(btnUpdateNow).toBeVisible();
|
||||
await btnUpdateNow.click();
|
||||
|
||||
// LoadingState -> Updating your device...
|
||||
await expect(getByText(page, 'general_update_updating_title')).toBeVisible();
|
||||
const update_type = m.general_update_application_type();
|
||||
await expect(getByText(page, 'general_update_status_downloading', { update_type })).toBeVisible();
|
||||
await expect(getByText(page, 'general_update_status_verifying', { update_type })).toBeVisible();
|
||||
await expect(getByText(page, 'general_update_status_installing', { update_type })).toBeVisible();
|
||||
await expect(getByText(page, 'general_update_status_awaiting_reboot')).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(30000);
|
||||
// await expect(page.getByText('Update available')).toBeVisible();
|
||||
|
||||
// await page.getByRole('button', { name: 'Update now' }).click();
|
||||
|
||||
// await expect(page.getByText('Updating...')).toBeVisible();
|
||||
|
||||
});
|
||||
|
||||
test('downgrade process', async ({ page }) => {
|
||||
await page.goto(`http://${TARGET_DEVICE_IP}`);
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(
|
||||
page,
|
||||
'Title should include JetKVM',
|
||||
).toHaveTitle(/JetKVM/);
|
||||
|
||||
// Except ICE gathering completed or JetKVM device connected
|
||||
await expect(
|
||||
getByText(page, 'ice_gathering_completed').or(getByText(page, 'peer_connection_connected')),
|
||||
'should be ICE gathering completed or JetKVM device connected',
|
||||
).toBeVisible();
|
||||
|
||||
// Except No HDMI signal detected, as the device is not connected to the HDMI port
|
||||
await expect(
|
||||
getByText(page, 'video_overlay_no_hdmi_signal'),
|
||||
'should be visible when no HDMI signal is detected',
|
||||
).toBeVisible();
|
||||
|
||||
// Emulate upgrade process
|
||||
await getByRole(page, 'button', 'action_bar_settings').click();
|
||||
await expect(
|
||||
getByText(page, 'general_page_description'),
|
||||
'should be visible when general page description is visible',
|
||||
).toBeVisible();
|
||||
|
||||
// Go to Advanced tab
|
||||
const btnAdvanced = getByRole(page, 'link', 'settings_advanced');
|
||||
await expect(
|
||||
btnAdvanced,
|
||||
'should be visible when advanced link is visible',
|
||||
).toBeVisible();
|
||||
await btnAdvanced.click();
|
||||
|
||||
// Now we're on the Advanced tab
|
||||
await expect(
|
||||
getByText(page, 'advanced_version_update_title'),
|
||||
'should be visible when advanced version update title is visible',
|
||||
).toBeVisible();
|
||||
|
||||
// choose the System only option
|
||||
const whatToUpdateLabel = page.locator('label', { hasText: m.advanced_version_update_target_label() });
|
||||
await expect(
|
||||
whatToUpdateLabel,
|
||||
'should be visible when what to update label is visible',
|
||||
).toBeVisible();
|
||||
const whatToUpdateSelect = page.locator('select');
|
||||
await expect(
|
||||
whatToUpdateSelect,
|
||||
'should be visible when what to update select is visible',
|
||||
).toBeVisible();
|
||||
await whatToUpdateSelect.selectOption('system');
|
||||
|
||||
// now, make sure the system version input is visible
|
||||
const systemVersionLabel = page.locator(
|
||||
'div',
|
||||
{ hasText: m.advanced_version_update_system_label(), hasNotText: m.advanced_version_update_target_label() },
|
||||
);
|
||||
await expect(
|
||||
systemVersionLabel,
|
||||
'SystemVersionLabel should be visible',
|
||||
).toBeVisible();
|
||||
const systemVersionInput = systemVersionLabel.getByRole('textbox');
|
||||
await expect(
|
||||
systemVersionInput,
|
||||
'SystemVersionInput should be visible',
|
||||
).toBeVisible();
|
||||
await systemVersionInput.fill('0.2.7');
|
||||
|
||||
// acknowledge the version change
|
||||
const versionChangeAcknowledgedLabel = page.locator(
|
||||
'label',
|
||||
{ hasText: "I understand version changes may break my device and require factory reset" },
|
||||
);
|
||||
await expect(
|
||||
versionChangeAcknowledgedLabel,
|
||||
).toBeVisible();
|
||||
const versionChangeAcknowledgedCheckbox = versionChangeAcknowledgedLabel.getByRole('checkbox');
|
||||
await expect(versionChangeAcknowledgedCheckbox).toBeVisible();
|
||||
await versionChangeAcknowledgedCheckbox.check();
|
||||
|
||||
// now, click the damn button
|
||||
const btnVersionUpdate = getByRole(page, 'button', 'advanced_version_update_button');
|
||||
await expect(btnVersionUpdate).toBeVisible();
|
||||
await btnVersionUpdate.click();
|
||||
|
||||
// Upgrade is a very time-consuming process, so we'll give it a generous timeout
|
||||
const timeout = 5 * 60 * 1000;
|
||||
|
||||
// LoadingState -> Checking for updates...
|
||||
await expect(getByText(page, 'general_update_checking_title'),
|
||||
'UpdatingDeviceState: checking for updates...',
|
||||
).toBeVisible({ timeout });
|
||||
|
||||
// UpdateAvailableState -> Prompt to update the device
|
||||
await expect(getByText(page, 'general_update_available_title'),
|
||||
'should be visible when general update available title is visible',
|
||||
).toBeVisible({ timeout });
|
||||
await expect(getByText(page, 'general_update_will_disable_auto_update_description'),
|
||||
'UpdateAvailableState: should show warning if it\'s a custom update',
|
||||
).toBeVisible();
|
||||
|
||||
const btnUpdateNow = getByRole(page, 'button', 'general_update_now_button');
|
||||
await expect(btnUpdateNow,
|
||||
'Update Now button should be visible',
|
||||
).toBeVisible();
|
||||
await btnUpdateNow.click();
|
||||
|
||||
// LoadingState -> Updating your device...
|
||||
await expect(getByText(page, 'general_update_updating_title'),
|
||||
'UpdatingDeviceState: title should be updating your device...',
|
||||
).toBeVisible();
|
||||
const update_type = m.general_update_system_type();
|
||||
|
||||
// UpdatingDeviceState -> Downloading, Verifying, Installing, Awaiting reboot or rebooting
|
||||
await expect(
|
||||
getByText(page, 'general_update_status_downloading', { update_type }).
|
||||
or(getByText(page, 'general_update_status_verifying', { update_type })).
|
||||
or(getByText(page, 'general_update_status_installing', { update_type })),
|
||||
'UpdatingDeviceState: downloading, verifying, installing',
|
||||
).toBeVisible({ timeout });
|
||||
await expect(
|
||||
getByText(page, 'general_update_status_awaiting_reboot')
|
||||
.or(getByText(page, 'general_update_rebooting')),
|
||||
'UpdatingDeviceState: awaiting reboot or rebooting',
|
||||
).toBeVisible({ timeout });
|
||||
|
||||
// Leaving device on
|
||||
// await expect(pageGetByText('updating_leave_device_on')).toBeVisible({ timeout });
|
||||
|
||||
// Rebooting or different IP message
|
||||
await expect(
|
||||
getByText(page, 'video_overlay_reboot_device_is_rebooting')
|
||||
.or(getByText(page, 'video_overlay_reboot_different_ip_message'))
|
||||
, 'VideoOverlay should show either reboot device is rebooting or different ip message'
|
||||
).toBeVisible({ timeout });
|
||||
|
||||
// Get the element indicating WebRTC connection status
|
||||
// TODO: use the role="status" to filter the element instead of the classNames
|
||||
const peerConnectionStatusCard = page.locator('.flex.items-center.gap-x-3.border.bg-white', { hasText: m.jetkvm_device() });
|
||||
// we're testing against the production build, so react locators aren't available here
|
||||
await expect(
|
||||
peerConnectionStatusCard,
|
||||
'PeerConnectionStatusCard should be visible'
|
||||
).toBeVisible({ timeout });
|
||||
|
||||
// it should transition to the disconnected state
|
||||
await expect(
|
||||
peerConnectionStatusCard.getByText(m.peer_connection_disconnected()),
|
||||
'PeerConnectionStatusCard should be transitioned to the DISCONNECTED state',
|
||||
).toBeVisible({ timeout });
|
||||
|
||||
// then, wait for the peer connection status card to transition to the CONNECTED state
|
||||
await expect(
|
||||
peerConnectionStatusCard.getByText(m.peer_connection_connected()),
|
||||
'PeerConnectionStatusCard should be transitioned to the CONNECTED state after reboot',
|
||||
).toBeVisible({ timeout });
|
||||
});
|
||||
|
|
@ -54,7 +54,8 @@
|
|||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"src",
|
||||
"tests/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue