This commit is contained in:
Siyuan Miao 2025-11-19 21:36:34 +01:00
parent c563a28613
commit 2bd9867d25
2 changed files with 152 additions and 121 deletions

View File

@ -5,9 +5,16 @@ 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 translate = (i18nKey: I18nKey, options?: I18nOptions) => {
const func = m[i18nKey as keyof typeof m] as (options?: I18nOptions) => string
const message = func(options);
// older version uses "..." instead of "…" for the ellipsis
// to avoid breaking the tests, we just remove the ellipsis
if (message.includes("…")) {
return message.replace("…", "");
}
return message;
};
export const getByText = (page: Page, i18nKey: I18nKey, options?: I18nOptions) => (
page.getByText(translate(i18nKey, options))
@ -17,4 +24,4 @@ export const getByRole = (page: Page, role: Parameters<typeof page.getByRole>[0]
page.getByRole(role, { name: translate(i18nKey) })
);
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms));

View File

@ -1,4 +1,4 @@
import { test, expect, PlaywrightTestArgs } from '@playwright/test';
import { test, expect, PlaywrightTestArgs, Page } from '@playwright/test';
import { m } from '../localization/paraglide/messages';
@ -6,122 +6,20 @@ import { getByRole, getByText, sleep } from './helper';
const TARGET_DEVICE_IP = "192.168.0.145"
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();
});
interface CustomUpgradeProcessOptions {
sys?: string;
app?: string;
resetConfig?: boolean;
}
const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => async ({ page }: PlaywrightTestArgs) => {
await page.goto(`http://${TARGET_DEVICE_IP}`);
// Expect a title "to contain" a substring.
await expect(
page,
'Title should include 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
await expect(
getByText(page, 'ice_gathering_completed').
or(getByText(page, 'peer_connection_connected').first()).
or(getByText(page, 'video_overlay_loading_stream'))
,
'Wait until the WebRTC connection is established',
).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();
await sleep(1000);
// 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();
const doCustomUpdateSelect = async (page: Page, sys?: string, app?: string) => {
// Go to Advanced tab
const btnAdvanced = getByRole(page, 'link', 'settings_advanced');
await expect(
btnAdvanced,
'should be visible when advanced link is visible',
).toBeVisible();
await sleep(1000);
await sleep();
await btnAdvanced.click();
// Now we're on the Advanced tab
@ -187,11 +85,11 @@ const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => asyn
// now, click the damn button
const btnVersionUpdate = getByRole(page, 'button', 'advanced_version_update_button');
await expect(btnVersionUpdate).toBeVisible();
await sleep(1000);
await sleep();
await btnVersionUpdate.click();
// Upgrade is a very time-consuming process, so we'll give it a generous timeout
const timeout = 5 * 60 * 1000;
// wait for 1 minute for the checkUpdate process to complete
const timeout = 1 * 60 * 1000;
// LoadingState -> Checking for updates...
await expect(getByText(page, 'general_update_checking_title'),
@ -210,14 +108,137 @@ const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => asyn
await expect(btnUpdateNow,
'Update Now button should be visible',
).toBeVisible();
await sleep(1000);
await sleep();
await btnUpdateNow.click();
}
const doStandardUpdate = async (page: Page) => {
const btnCheckForUpdates = getByRole(page, 'button', 'general_check_for_updates');
await expect(btnCheckForUpdates).toBeVisible();
await sleep();
await btnCheckForUpdates.click();
// LoadingState -> Checking for updates...
// sometimes it would be too fast to check for updates, so we just check both titles here
await expect(
getByText(page, 'general_update_checking_title').
or(getByText(page, 'general_update_available_title')).
or(getByText(page, 'general_update_up_to_date_title')),
'LoadingState: checking for updates or update available',
).toBeVisible();
// check if system is up to date
if (await getByText(page, 'general_update_up_to_date_title').isVisible()) {
test.skip(true, 'System is up to date, skipping update process');
return;
}
// UpdateAvailableState -> Prompt to update the device
const btnUpdateNow = getByRole(page, 'button', 'general_update_now_button');
await expect(btnUpdateNow).toBeVisible();
// Check which updates are going to be installed
const updateAvailableLabel = page.locator('.mb-2.text-sm', { hasText: m.general_update_available_description() }).first();
await expect(updateAvailableLabel).toBeVisible();
const updateAvailableContainer = page.locator('.text-left', { has: updateAvailableLabel }).first();
await expect(updateAvailableContainer).toBeVisible();
const versionInfo = {
sys: '',
app: '',
}
const systemUpdateAvailable = updateAvailableContainer.locator('.text-sm', { hasText: m.general_update_system_type()+":" }).first();
if (await systemUpdateAvailable.isVisible()) {
versionInfo.sys = await systemUpdateAvailable.textContent() ?? '';
}
const appUpdateAvailable = updateAvailableContainer.locator('.text-sm', { hasText: m.general_update_application_type()+":" }).first();
if (await appUpdateAvailable.isVisible()) {
versionInfo.app = await appUpdateAvailable.textContent() ?? '';
}
await sleep();
await btnUpdateNow.click();
return versionInfo;
}
const runUpdateTest = ({ sys, app }: CustomUpgradeProcessOptions) => async ({ page }: PlaywrightTestArgs) => {
await page.goto(`http://${TARGET_DEVICE_IP}`);
// Expect a title "to contain" a substring.
await expect(
page,
'Title should include 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();
// click the continue button
await getByRole(page, 'button', 'continue').click();
}
// Except ICE gathering completed or JetKVM device connected
await expect(
getByText(page, 'ice_gathering_completed').
or(getByText(page, 'peer_connection_connected').first()).
or(getByText(page, 'video_overlay_loading_stream'))
,
'Wait until the WebRTC connection is established',
).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();
await sleep();
// 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();
let [sysUpdatePending, appUpdatePending] = [false, false];
// If sys or app is provided, we'll do a custom update, otherwise, we'll do a standard update
if (sys || app) {
await doCustomUpdateSelect(page, sys, app);
sysUpdatePending = sys !== undefined;
appUpdatePending = app !== undefined;
} else {
const versionInfo = await doStandardUpdate(page);
sysUpdatePending = versionInfo?.sys !== '';
appUpdatePending = versionInfo?.app !== '';
}
// LoadingState -> Updating your device...
await expect(getByText(page, 'general_update_updating_title'),
'UpdatingDeviceState: title should be updating your device...',
).toBeVisible();
// update is a very time-consuming process, so we'll give it a generous timeout
const timeout = 10 * 60 * 1000; // 10 minutes
// UpdatingDeviceState -> Downloading, Verifying, Installing, Awaiting reboot or rebooting
const waitingForUpdate = async (update_type: string = m.general_update_system_type()) => {
await expect(
@ -228,14 +249,16 @@ const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => asyn
).toBeVisible({ timeout });
await expect(
getByText(page, 'general_update_status_awaiting_reboot')
.or(getByText(page, 'general_update_rebooting')),
.or(getByText(page, 'general_update_rebooting'))
// older version uses "..." instead of "…" for the ellipsis
.or(page.getByText('Rebooting to complete the update')),
'UpdatingDeviceState: awaiting reboot or rebooting',
).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());
if (appUpdatePending) await waitingForUpdate(m.general_update_application_type());
if (sysUpdatePending) await waitingForUpdate(m.general_update_system_type());
// Leaving device on
// await expect(pageGetByText('updating_leave_device_on')).toBeVisible({ timeout });
@ -277,16 +300,17 @@ const customUpgradeProcess = ({ sys, app }: CustomUpgradeProcessOptions) => asyn
).toBeVisible({ timeout });
};
test('standard upgrade process', runUpdateTest({}));
test('custom upgrade process: upgrade system only', customUpgradeProcess({
test('custom upgrade process: upgrade system only', runUpdateTest({
sys: '0.2.7',
}));
test('custom upgrade process: upgrade app only', customUpgradeProcess({
test('custom upgrade process: upgrade app only', runUpdateTest({
app: '0.4.5',
}));
test('custom upgrade process: upgrade both', customUpgradeProcess({
test('custom upgrade process: upgrade both', runUpdateTest({
sys: '0.2.7',
app: '0.4.5',
app: '0.4.8',
}));