feat: OTA test

This commit is contained in:
Siyuan Miao 2025-11-19 19:45:20 +01:00
parent 64a514520c
commit c563a28613
4 changed files with 130 additions and 41 deletions

View File

@ -19,5 +19,8 @@
], ],
"url": "./internal/ota/testdata/ota.schema.json" "url": "./internal/ota/testdata/ota.schema.json"
} }
],
"i18n-ally.localesPaths": [
"ui/localization/messages"
] ]
} }

View File

@ -1,5 +1,7 @@
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from '@playwright/test';
const width = 1728, height = 996;
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
@ -31,7 +33,12 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on', trace: 'on',
video: 'on', video: {
mode: 'on',
size: {
width, height,
}
}
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
@ -39,7 +46,13 @@ export default defineConfig({
{ {
name: 'chromium', name: 'chromium',
// Headless must be disabled otherwise WebRTC will not work properly // Headless must be disabled otherwise WebRTC will not work properly
use: { ...devices['Desktop Chrome'], headless: false }, use: {
...devices['Desktop Chrome'],
headless: false,
viewport: {
width, height,
}
}
}, },
// { // {

View File

@ -17,3 +17,4 @@ export const getByRole = (page: Page, role: Parameters<typeof page.getByRole>[0]
page.getByRole(role, { name: translate(i18nKey) }) page.getByRole(role, { name: translate(i18nKey) })
); );
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View File

@ -1,10 +1,10 @@
import { test, expect } from '@playwright/test'; import { test, expect, PlaywrightTestArgs } from '@playwright/test';
import { m } from '../localization/paraglide/messages'; import { m } from '../localization/paraglide/messages';
import { getByRole, getByText } from './helper'; import { getByRole, getByText, sleep } from './helper';
const TARGET_DEVICE_IP = "192.168.2.4" const TARGET_DEVICE_IP = "192.168.0.145"
test('upgrade process', async ({ page }) => { test('upgrade process', async ({ page }) => {
await page.goto(`http://${TARGET_DEVICE_IP}`); await page.goto(`http://${TARGET_DEVICE_IP}`);
@ -58,7 +58,12 @@ test('upgrade process', async ({ page }) => {
}); });
test('downgrade process', async ({ page }) => { interface CustomUpgradeProcessOptions {
sys?: string;
app?: string;
}
const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => async ({ page }: PlaywrightTestArgs) => {
await page.goto(`http://${TARGET_DEVICE_IP}`); await page.goto(`http://${TARGET_DEVICE_IP}`);
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
@ -67,17 +72,41 @@ test('downgrade process', async ({ page }) => {
'Title should include JetKVM', 'Title should include JetKVM',
).toHaveTitle(/JetKVM/); ).toHaveTitle(/JetKVM/);
// If it's showing Welcome screen, we'll do the setup process,
// otherwise, we'll skip the setup process and go to the next step
await page.waitForLoadState('networkidle');
if (await getByText(page, 'welcome_to_jetkvm').isVisible()) {
// check if current URL is /welcome
await page.waitForURL('**/welcome');
// click the setup button (LinkButton)
await getByRole(page, 'link', 'jetkvm_setup').click();
// check if current URL is /welcome/mode
await page.waitForURL('**/welcome/mode');
// click the local authentication method button
await page.locator('input[name="localAuthMode"][value="noPassword"]').click();
// wait for 1 second to ensure the checkbox is checked
await sleep(1000);
// click the continue button
await getByRole(page, 'button', 'continue').click();
}
// Except ICE gathering completed or JetKVM device connected // Except ICE gathering completed or JetKVM device connected
await expect( await expect(
getByText(page, 'ice_gathering_completed').or(getByText(page, 'peer_connection_connected')), getByText(page, 'ice_gathering_completed').
'should be ICE gathering completed or JetKVM device connected', or(getByText(page, 'peer_connection_connected').first()).
or(getByText(page, 'video_overlay_loading_stream'))
,
'Wait until the WebRTC connection is established',
).toBeVisible(); ).toBeVisible();
// Except No HDMI signal detected, as the device is not connected to the HDMI port // Except No HDMI signal detected, as the device is not connected to the HDMI port
await expect( // await expect(
getByText(page, 'video_overlay_no_hdmi_signal'), // getByText(page, 'video_overlay_no_hdmi_signal'),
'should be visible when no HDMI signal is detected', // 'should be visible when no HDMI signal is detected',
).toBeVisible(); // ).toBeVisible();
await sleep(1000);
// Emulate upgrade process // Emulate upgrade process
await getByRole(page, 'button', 'action_bar_settings').click(); await getByRole(page, 'button', 'action_bar_settings').click();
@ -92,6 +121,7 @@ test('downgrade process', async ({ page }) => {
btnAdvanced, btnAdvanced,
'should be visible when advanced link is visible', 'should be visible when advanced link is visible',
).toBeVisible(); ).toBeVisible();
await sleep(1000);
await btnAdvanced.click(); await btnAdvanced.click();
// Now we're on the Advanced tab // Now we're on the Advanced tab
@ -111,23 +141,36 @@ test('downgrade process', async ({ page }) => {
whatToUpdateSelect, whatToUpdateSelect,
'should be visible when what to update select is visible', 'should be visible when what to update select is visible',
).toBeVisible(); ).toBeVisible();
if (sys && app) {
await whatToUpdateSelect.selectOption('both');
} else if (sys) {
await whatToUpdateSelect.selectOption('system'); await whatToUpdateSelect.selectOption('system');
} else if (app) {
await whatToUpdateSelect.selectOption('app');
}
if (sys) {
// now, make sure the system version input is visible // now, make sure the system version input is visible
const systemVersionLabel = page.locator( const systemVersionLabel = page.locator(
'div', 'div',
{ hasText: m.advanced_version_update_system_label(), hasNotText: m.advanced_version_update_target_label() }, { hasText: m.advanced_version_update_system_label(), hasNotText: m.advanced_version_update_target_label() },
); );
await expect( await expect(systemVersionLabel, 'SystemVersionLabel should be visible').toBeVisible();
systemVersionLabel,
'SystemVersionLabel should be visible',
).toBeVisible();
const systemVersionInput = systemVersionLabel.getByRole('textbox'); const systemVersionInput = systemVersionLabel.getByRole('textbox');
await expect( await expect(systemVersionInput, 'SystemVersionInput should be visible').toBeVisible();
systemVersionInput, await systemVersionInput.fill(sys);
'SystemVersionInput should be visible', }
).toBeVisible();
await systemVersionInput.fill('0.2.7'); if (app) {
const appVersionLabel = page.locator(
'div',
{ hasText: m.advanced_version_update_app_label(), hasNotText: m.advanced_version_update_target_label() },
);
await expect(appVersionLabel, 'AppVersionLabel should be visible').toBeVisible();
const appVersionInput = appVersionLabel.getByRole('textbox');
await expect(appVersionInput, 'AppVersionInput should be visible').toBeVisible();
await appVersionInput.fill(app);
}
// acknowledge the version change // acknowledge the version change
const versionChangeAcknowledgedLabel = page.locator( const versionChangeAcknowledgedLabel = page.locator(
@ -144,6 +187,7 @@ test('downgrade process', async ({ page }) => {
// now, click the damn button // now, click the damn button
const btnVersionUpdate = getByRole(page, 'button', 'advanced_version_update_button'); const btnVersionUpdate = getByRole(page, 'button', 'advanced_version_update_button');
await expect(btnVersionUpdate).toBeVisible(); await expect(btnVersionUpdate).toBeVisible();
await sleep(1000);
await btnVersionUpdate.click(); await btnVersionUpdate.click();
// Upgrade is a very time-consuming process, so we'll give it a generous timeout // Upgrade is a very time-consuming process, so we'll give it a generous timeout
@ -166,15 +210,16 @@ test('downgrade process', async ({ page }) => {
await expect(btnUpdateNow, await expect(btnUpdateNow,
'Update Now button should be visible', 'Update Now button should be visible',
).toBeVisible(); ).toBeVisible();
await sleep(1000);
await btnUpdateNow.click(); await btnUpdateNow.click();
// LoadingState -> Updating your device... // LoadingState -> Updating your device...
await expect(getByText(page, 'general_update_updating_title'), await expect(getByText(page, 'general_update_updating_title'),
'UpdatingDeviceState: title should be updating your device...', 'UpdatingDeviceState: title should be updating your device...',
).toBeVisible(); ).toBeVisible();
const update_type = m.general_update_system_type();
// UpdatingDeviceState -> Downloading, Verifying, Installing, Awaiting reboot or rebooting // UpdatingDeviceState -> Downloading, Verifying, Installing, Awaiting reboot or rebooting
const waitingForUpdate = async (update_type: string = m.general_update_system_type()) => {
await expect( await expect(
getByText(page, 'general_update_status_downloading', { update_type }). getByText(page, 'general_update_status_downloading', { update_type }).
or(getByText(page, 'general_update_status_verifying', { update_type })). or(getByText(page, 'general_update_status_verifying', { update_type })).
@ -186,6 +231,11 @@ test('downgrade process', async ({ page }) => {
.or(getByText(page, 'general_update_rebooting')), .or(getByText(page, 'general_update_rebooting')),
'UpdatingDeviceState: awaiting reboot or rebooting', 'UpdatingDeviceState: awaiting reboot or rebooting',
).toBeVisible({ timeout }); ).toBeVisible({ timeout });
};
// Application update always comes first
if (app) await waitingForUpdate(m.general_update_application_type());
if (sys) await waitingForUpdate(m.general_update_system_type());
// Leaving device on // Leaving device on
// await expect(pageGetByText('updating_leave_device_on')).toBeVisible({ timeout }); // await expect(pageGetByText('updating_leave_device_on')).toBeVisible({ timeout });
@ -217,4 +267,26 @@ test('downgrade process', async ({ page }) => {
peerConnectionStatusCard.getByText(m.peer_connection_connected()), peerConnectionStatusCard.getByText(m.peer_connection_connected()),
'PeerConnectionStatusCard should be transitioned to the CONNECTED state after reboot', 'PeerConnectionStatusCard should be transitioned to the CONNECTED state after reboot',
).toBeVisible({ timeout }); ).toBeVisible({ timeout });
});
// then, we should see a message saying that the update is completed successfully
await expect(getByText(page, 'general_update_completed_title'),
'UpdateCompletedState: title should be update completed successfully',
).toBeVisible({ timeout });
await expect(getByText(page, 'general_update_completed_description'),
'UpdateCompletedState: description should be update completed successfully',
).toBeVisible({ timeout });
};
test('custom upgrade process: upgrade system only', customUpgradeProcess({
sys: '0.2.7',
}));
test('custom upgrade process: upgrade app only', customUpgradeProcess({
app: '0.4.5',
}));
test('custom upgrade process: upgrade both', customUpgradeProcess({
sys: '0.2.7',
app: '0.4.5',
}));