From 2bd9867d25689f421d76e09322e3ed0b450eccd3 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Wed, 19 Nov 2025 21:36:34 +0100 Subject: [PATCH] fix --- ui/tests/helper.ts | 15 ++- ui/tests/ota.spec.ts | 258 +++++++++++++++++++++++-------------------- 2 files changed, 152 insertions(+), 121 deletions(-) diff --git a/ui/tests/helper.ts b/ui/tests/helper.ts index c7d68005..10e387f0 100644 --- a/ui/tests/helper.ts +++ b/ui/tests/helper.ts @@ -5,9 +5,16 @@ import { m } from "../localization/paraglide/messages.js"; type I18nKey = keyof typeof m; type I18nOptions = Record; -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[0] page.getByRole(role, { name: translate(i18nKey) }) ); -export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file +export const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file diff --git a/ui/tests/ota.spec.ts b/ui/tests/ota.spec.ts index 90ac60f7..27defc35 100644 --- a/ui/tests/ota.spec.ts +++ b/ui/tests/ota.spec.ts @@ -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', })); \ No newline at end of file