diff --git a/src/app/src/types/factory_reset.rs b/src/app/src/types/factory_reset.rs index f2d604b..985f0c9 100644 --- a/src/app/src/types/factory_reset.rs +++ b/src/app/src/types/factory_reset.rs @@ -5,22 +5,39 @@ use std::fmt; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "camelCase")] pub enum FactoryResetStatus { + /// No result received yet. Distinct from `Unrecognized` so the UI can stay + /// silent while a result is missing and still report one it cannot name. #[default] Unknown, - ModeSupported, - ModeUnsupported, - BackupRestoreError, - ConfigurationError, + Success, + Invalid, + Error, + ConfigError, + /// Reset succeeded, but a partition needed a second format attempt. + Warning, + /// A status code this version does not know. + Unrecognized, +} + +impl FactoryResetStatus { + /// `Warning` counts as success: the reset completed, only a partition + /// needed a retry. + #[must_use] + pub const fn is_success(self) -> bool { + matches!(self, Self::Success | Self::Warning) + } } impl fmt::Display for FactoryResetStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Unknown => write!(f, "unknown"), - Self::ModeSupported => write!(f, "modeSupported"), - Self::ModeUnsupported => write!(f, "modeUnsupported"), - Self::BackupRestoreError => write!(f, "backupRestoreError"), - Self::ConfigurationError => write!(f, "configurationError"), + Self::Success => write!(f, "success"), + Self::Invalid => write!(f, "invalid"), + Self::Error => write!(f, "error"), + Self::ConfigError => write!(f, "configError"), + Self::Warning => write!(f, "warning"), + Self::Unrecognized => write!(f, "unrecognized"), } } } @@ -31,8 +48,11 @@ impl fmt::Display for FactoryResetStatus { pub struct FactoryResetResult { pub status: FactoryResetStatus, pub context: Option, - pub error: String, + pub error: Option, pub paths: Vec, + /// `true` once the reset started wiping data. On a failure this separates a + /// safe abort from one that left the device half wiped. + pub data_wiped: bool, } /// Factory reset state from WebSocket diff --git a/src/app/src/types/ods.rs b/src/app/src/types/ods.rs index 1770015..da872b3 100644 --- a/src/app/src/types/ods.rs +++ b/src/app/src/types/ods.rs @@ -170,34 +170,48 @@ impl From for NetworkStatus { } } -/// Factory reset result status — ODS sends numeric values (`serde_repr`) +/// Factory reset result status — ODS sends numeric values (`serde_repr`). +/// `u32` matches the serialization in omnect-os-init. #[derive(Debug, Clone, Deserialize_repr, PartialEq, Eq)] -#[repr(u8)] +#[repr(u32)] pub enum OdsFactoryResetResultStatus { - ModeSupported = 0, - ModeUnsupported = 1, - BackupRestoreError = 2, - ConfigurationError = 3, + Success = 0, + Invalid = 1, + Error = 2, + ConfigError = 3, + Warning = 4, + /// A future status code must parse instead of failing the whole update. + #[serde(other)] + Unrecognized = u32::MAX, } impl From for FactoryResetStatus { fn from(ods: OdsFactoryResetResultStatus) -> Self { match ods { - OdsFactoryResetResultStatus::ModeSupported => Self::ModeSupported, - OdsFactoryResetResultStatus::ModeUnsupported => Self::ModeUnsupported, - OdsFactoryResetResultStatus::BackupRestoreError => Self::BackupRestoreError, - OdsFactoryResetResultStatus::ConfigurationError => Self::ConfigurationError, + OdsFactoryResetResultStatus::Success => Self::Success, + OdsFactoryResetResultStatus::Invalid => Self::Invalid, + OdsFactoryResetResultStatus::Error => Self::Error, + OdsFactoryResetResultStatus::ConfigError => Self::ConfigError, + OdsFactoryResetResultStatus::Warning => Self::Warning, + OdsFactoryResetResultStatus::Unrecognized => Self::Unrecognized, } } } -/// Factory reset result +/// Factory reset result. +/// ODS sends twin reports as merge patches, so every optional key arrives +/// explicitly as `null` rather than being omitted. #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct OdsFactoryResetResult { pub status: OdsFactoryResetResultStatus, + #[serde(default)] pub context: Option, - pub error: String, + #[serde(default)] + pub error: Option, + #[serde(default)] pub paths: Vec, + #[serde(default)] + pub data_wiped: bool, } impl From for FactoryResetResult { @@ -207,6 +221,7 @@ impl From for FactoryResetResult { context: ods.context, error: ods.error, paths: ods.paths, + data_wiped: ods.data_wiped, } } } diff --git a/src/app/src/update/device/network/verification.rs b/src/app/src/update/device/network/verification.rs index a348c58..31f82f7 100644 --- a/src/app/src/update/device/network/verification.rs +++ b/src/app/src/update/device/network/verification.rs @@ -479,10 +479,11 @@ mod tests { factory_reset: Some(FactoryReset { keys: vec!["key1".to_string()], result: Some(FactoryResetResult { - status: FactoryResetStatus::ModeSupported, + status: FactoryResetStatus::Success, context: None, - error: String::new(), + error: None, paths: vec![], + data_wiped: true, }), }), ..Default::default() diff --git a/src/app/src/update/websocket.rs b/src/app/src/update/websocket.rs index cd56b00..7fe880e 100644 --- a/src/app/src/update/websocket.rs +++ b/src/app/src/update/websocket.rs @@ -199,20 +199,104 @@ mod tests { assert_eq!(model.factory_reset, Some(expected_status)); } - #[test] - fn parses_integer_status_from_ods() { + fn parse_result(json: &str) -> crate::types::FactoryResetResult { let mut model = Model::default(); + let _ = handle(WebSocketEvent::FactoryResetUpdated(json.into()), &mut model); + model + .factory_reset + .expect("factory_reset should be set") + .result + .expect("result should be set") + } + + #[test] + fn parses_successful_result() { + let result = parse_result( + r#"{"keys":["network"],"result":{"status":0,"error":null,"context":null,"paths":["network"],"data_wiped":true}}"#, + ); - // ODS sends status as integer (serde_repr): 0=ModeSupported, 1=ModeUnsupported, etc. - let json = r#"{"keys":["network"],"result":{"status":0,"error":"0","paths":["/etc/systemd/network/"]}}"#; + assert_eq!(result.status, FactoryResetStatus::Success); + assert!(result.status.is_success()); + assert_eq!(result.error, None); + assert_eq!(result.context, None); + assert_eq!(result.paths, vec!["network"]); + assert!(result.data_wiped); + } - let _ = handle(WebSocketEvent::FactoryResetUpdated(json.into()), &mut model); + #[test] + fn parses_error_result_with_message() { + let result = parse_result( + r#"{"keys":["network"],"result":{"status":2,"error":"format failed","context":"factory partition","paths":[],"data_wiped":true}}"#, + ); - let factory_reset = model.factory_reset.expect("factory_reset should be set"); - let result = factory_reset.result.expect("result should be set"); - assert_eq!(result.status, FactoryResetStatus::ModeSupported); - assert_eq!(result.error, "0"); - assert_eq!(result.paths, vec!["/etc/systemd/network/"]); + assert_eq!(result.status, FactoryResetStatus::Error); + assert!(!result.status.is_success()); + assert_eq!(result.error.as_deref(), Some("format failed")); + assert_eq!(result.context.as_deref(), Some("factory partition")); + assert!(result.data_wiped); + } + + #[test] + fn warning_counts_as_success() { + let result = parse_result( + r#"{"keys":["network"],"result":{"status":4,"error":null,"context":"second format attempt","paths":[],"data_wiped":true}}"#, + ); + + assert_eq!(result.status, FactoryResetStatus::Warning); + assert!(result.status.is_success()); + } + + #[test] + fn abort_before_wipe_reports_data_not_wiped() { + let result = parse_result( + r#"{"keys":["network"],"result":{"status":1,"error":"unknown preserve key","context":null,"paths":[],"data_wiped":false}}"#, + ); + + assert_eq!(result.status, FactoryResetStatus::Invalid); + assert!(!result.data_wiped); + } + + #[test] + fn config_error_maps_to_config_error() { + let result = parse_result( + r#"{"keys":["network"],"result":{"status":3,"error":"bad config","context":null,"paths":[],"data_wiped":false}}"#, + ); + + assert_eq!(result.status, FactoryResetStatus::ConfigError); + } + + #[test] + fn unknown_status_code_parses_as_unrecognized() { + // ODS reports a status it does not know itself as u32::MAX; any + // future code must not fail the whole update. + let result = parse_result( + r#"{"keys":["network"],"result":{"status":4294967295,"error":null,"context":null,"paths":[],"data_wiped":false}}"#, + ); + + assert_eq!(result.status, FactoryResetStatus::Unrecognized); + assert!(!result.status.is_success()); + } + + #[test] + fn unrecognized_status_is_distinct_from_no_result() { + // The UI stays silent on Unknown (no result yet) but must still + // report a result whose status it cannot name. + assert_ne!( + FactoryResetStatus::Unrecognized, + FactoryResetStatus::Unknown + ); + assert_eq!(FactoryResetStatus::default(), FactoryResetStatus::Unknown); + } + + #[test] + fn missing_optional_keys_fall_back_to_defaults() { + let result = parse_result(r#"{"keys":["network"],"result":{"status":0}}"#); + + assert_eq!(result.status, FactoryResetStatus::Success); + assert_eq!(result.error, None); + assert_eq!(result.context, None); + assert!(result.paths.is_empty()); + assert!(!result.data_wiped); } } diff --git a/src/backend/src/omnect_device_service_client.rs b/src/backend/src/omnect_device_service_client.rs index c5721d7..524be52 100644 --- a/src/backend/src/omnect_device_service_client.rs +++ b/src/backend/src/omnect_device_service_client.rs @@ -156,7 +156,11 @@ impl Clone for MockDeviceServiceClient { } impl OmnectDeviceServiceClient { - const REQUIRED_CLIENT_VERSION: &str = ">=0.39.0"; + // The floor is the oldest omnect-device-service that publishes the + // factory-reset result in the shape parsed here (feature version 4). + // TODO: remove me, as soon as the omnect-device-service release carrying + // factory-reset feature version 4 is tagged — replace 0.46.0 with that tag. + const REQUIRED_CLIENT_VERSION: &str = ">=0.46.0"; // API endpoint constants const STATUS_ENDPOINT: &str = "/status/v1"; @@ -406,15 +410,15 @@ mod tests { #[test] fn required_version_parses_correctly() { let version_req = OmnectDeviceServiceClient::required_version(); - assert_eq!(version_req.to_string(), ">=0.39.0"); + assert_eq!(version_req.to_string(), ">=0.46.0"); } #[test] fn required_version_matches_valid_versions() { let version_req = OmnectDeviceServiceClient::required_version(); - assert!(version_req.matches(&Version::parse("0.39.0").unwrap())); - assert!(version_req.matches(&Version::parse("0.40.0").unwrap())); + assert!(version_req.matches(&Version::parse("0.46.0").unwrap())); + assert!(version_req.matches(&Version::parse("0.47.0").unwrap())); assert!(version_req.matches(&Version::parse("1.0.0").unwrap())); } @@ -422,8 +426,10 @@ mod tests { fn required_version_rejects_older_versions() { let version_req = OmnectDeviceServiceClient::required_version(); - assert!(!version_req.matches(&Version::parse("0.38.9").unwrap())); - assert!(!version_req.matches(&Version::parse("0.30.0").unwrap())); + // 0.45.2 is the last release publishing the factory-reset result in + // the previous shape. + assert!(!version_req.matches(&Version::parse("0.45.2").unwrap())); + assert!(!version_req.matches(&Version::parse("0.41.0").unwrap())); assert!(!version_req.matches(&Version::parse("0.1.0").unwrap())); } } @@ -449,7 +455,7 @@ mod tests { #[test] fn detects_version_mismatch_when_below_requirement() { - let status = create_test_status("0.38.0"); + let status = create_test_status("0.45.2"); let current_version = status.system_info.omnect_device_service_version; let required_version = OmnectDeviceServiceClient::required_version(); @@ -462,7 +468,7 @@ mod tests { #[test] fn detects_no_mismatch_when_matching_requirement() { - let status = create_test_status("0.40.0"); + let status = create_test_status("0.46.0"); let current_version = status.system_info.omnect_device_service_version; let required_version = OmnectDeviceServiceClient::required_version(); diff --git a/src/ui/src/App.vue b/src/ui/src/App.vue index 5bc8afb..d56a29a 100644 --- a/src/ui/src/App.vue +++ b/src/ui/src/App.vue @@ -8,6 +8,7 @@ import OmnectLogo from "./components/branding/OmnectLogo.vue" import OverlaySpinner from "./components/feedback/OverlaySpinner.vue" import UserMenu from "./components/UserMenu.vue" import { useCore } from "./composables/useCore" +import { isFactoryResetSuccess } from "./composables/core/types" import { useSnackbar } from "./composables/useSnackbar" import { useMessageWatchers } from "./composables/useMessageWatchers" @@ -93,6 +94,7 @@ const acknowledgeUpdateValidation = () => { const factoryResetModalSuccess = ref(false) const factoryResetError = ref(null) const factoryResetContext = ref(null) +const factoryResetDataWiped = ref(false) const updateValidationIsRollback = ref(false) // Watch authentication state to redirect to login if session is lost @@ -132,7 +134,8 @@ watch( // Snapshot once so the template is decoupled from the live ViewModel during close animation factoryResetError.value = result.error ?? null factoryResetContext.value = result.context ?? null - factoryResetModalSuccess.value = result.status === 'modeSupported' + factoryResetModalSuccess.value = isFactoryResetSuccess(result.status) + factoryResetDataWiped.value = result.dataWiped showFactoryResetResultModal.value = true } } @@ -200,6 +203,10 @@ watch(
OK diff --git a/src/ui/src/composables/core/sync.ts b/src/ui/src/composables/core/sync.ts index c827b5e..0b2407e 100644 --- a/src/ui/src/composables/core/sync.ts +++ b/src/ui/src/composables/core/sync.ts @@ -128,8 +128,9 @@ export function updateViewModelFromCore(): void { ? { status: factoryResetStatusToString(coreViewModel.factoryReset.result.status), context: coreViewModel.factoryReset.result.context || null, - error: coreViewModel.factoryReset.result.error, + error: coreViewModel.factoryReset.result.error || null, paths: coreViewModel.factoryReset.result.paths, + dataWiped: coreViewModel.factoryReset.result.dataWiped, } : null, } diff --git a/src/ui/src/composables/core/types.ts b/src/ui/src/composables/core/types.ts index 8c1b137..dd9c0b6 100644 --- a/src/ui/src/composables/core/types.ts +++ b/src/ui/src/composables/core/types.ts @@ -63,10 +63,12 @@ import { NetworkFormStateVariantsubmitting, FactoryResetStatus, FactoryResetStatusVariantunknown, - FactoryResetStatusVariantmodeSupported, - FactoryResetStatusVariantmodeUnsupported, - FactoryResetStatusVariantbackupRestoreError, - FactoryResetStatusVariantconfigurationError, + FactoryResetStatusVariantsuccess, + FactoryResetStatusVariantinvalid, + FactoryResetStatusVarianterror, + FactoryResetStatusVariantconfigError, + FactoryResetStatusVariantwarning, + FactoryResetStatusVariantunrecognized, UploadState, UploadStateVariantidle, UploadStateVariantuploading, @@ -215,7 +217,7 @@ export type WifiStateType = connectPollAttempt: number } -export type FactoryResetStatusString = 'unknown' | 'modeSupported' | 'modeUnsupported' | 'backupRestoreError' | 'configurationError' +export type FactoryResetStatusString = 'unknown' | 'success' | 'invalid' | 'error' | 'configError' | 'warning' | 'unrecognized' // ============================================================================ // ViewModel Interface @@ -238,8 +240,9 @@ export interface ViewModel { result: { status: FactoryResetStatusString context: string | null - error: string + error: string | null paths: string[] + dataWiped: boolean } | null } | null updateValidationStatus: { status: string } | null @@ -312,13 +315,23 @@ export interface ViewModel { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function factoryResetStatusToString(status: any): FactoryResetStatusString { if (status instanceof FactoryResetStatusVariantunknown) return 'unknown' - if (status instanceof FactoryResetStatusVariantmodeSupported) return 'modeSupported' - if (status instanceof FactoryResetStatusVariantmodeUnsupported) return 'modeUnsupported' - if (status instanceof FactoryResetStatusVariantbackupRestoreError) return 'backupRestoreError' - if (status instanceof FactoryResetStatusVariantconfigurationError) return 'configurationError' + if (status instanceof FactoryResetStatusVariantsuccess) return 'success' + if (status instanceof FactoryResetStatusVariantinvalid) return 'invalid' + if (status instanceof FactoryResetStatusVarianterror) return 'error' + if (status instanceof FactoryResetStatusVariantconfigError) return 'configError' + if (status instanceof FactoryResetStatusVariantwarning) return 'warning' + if (status instanceof FactoryResetStatusVariantunrecognized) return 'unrecognized' return 'unknown' } +/** + * A warning means the reset completed, only a partition needed a second format + * attempt — the user sees it as success. + */ +export function isFactoryResetSuccess(status: FactoryResetStatusString): boolean { + return status === 'success' || status === 'warning' +} + /** * Convert DeviceOperationState variant to typed object */ diff --git a/src/ui/tests/factory-reset.spec.ts b/src/ui/tests/factory-reset.spec.ts index 7fbbe34..f5ffad1 100644 --- a/src/ui/tests/factory-reset.spec.ts +++ b/src/ui/tests/factory-reset.spec.ts @@ -147,10 +147,10 @@ test.describe('Device Factory Reset - Reconnection', () => { await expect(page.getByText('Common Info')).toBeVisible({ timeout: 10000 }); // ODS publishes the factory reset result via WebSocket after republishing. - // status=0 maps to OdsFactoryResetResultStatus::ModeSupported → factoryResetIsSuccess=true. + // status=0 is Success; error and context are null when nothing went wrong. await publishToWebsocket('FactoryResetV1', { keys: ['network'], - result: { status: 0, error: '0', paths: ['/etc/systemd/network/'] }, + result: { status: 0, error: null, context: null, paths: ['network'], data_wiped: true }, }); await expect(page.getByText('Factory Reset Completed', { exact: true })).toBeVisible({ timeout: 5000 }); @@ -168,10 +168,10 @@ test.describe('Device Factory Reset - Reconnection', () => { await route.fulfill({ status: 200 }); }); - // Trigger the factory reset success modal via WebSocket (status: 0 = ModeSupported) + // Trigger the factory reset success modal via WebSocket (status: 0 = Success) await publishToWebsocket('FactoryResetV1', { keys: ['network'], - result: { status: 0, error: '0', paths: ['/etc/systemd/network/'] }, + result: { status: 0, error: null, context: null, paths: ['network'], data_wiped: true }, }); await expect(page.getByText('Factory Reset Completed', { exact: true })).toBeVisible({ timeout: 5000 }); @@ -197,4 +197,84 @@ test.describe('Device Factory Reset - Reconnection', () => { }); expect(flashDetected).toBe(false); }); + + test('warning status shows the success modal', async ({ page }) => { + await mockPortalAuth(page); + await setupAndLogin(page, { factoryResetResultAcked: false }); + + // status=4 is Warning: the reset worked, a partition just needed a second + // format attempt. + await publishToWebsocket('FactoryResetV1', { + keys: ['network'], + result: { + status: 4, + error: null, + context: 'second format attempt', + paths: ['network'], + data_wiped: true, + }, + }); + + await expect(page.getByText('Factory Reset Completed', { exact: true })).toBeVisible({ timeout: 5000 }); + }); + + test('failure after data was wiped warns the device needs another reset', async ({ page }) => { + await mockPortalAuth(page); + await setupAndLogin(page, { factoryResetResultAcked: false }); + + await publishToWebsocket('FactoryResetV1', { + keys: ['network'], + result: { + status: 2, + error: 'format failed', + context: 'factory partition', + paths: [], + data_wiped: true, + }, + }); + + await expect(page.getByText('Factory Reset Failed', { exact: true })).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('format failed')).toBeVisible(); + await expect(page.getByTestId('factory-reset-data-wiped')).toBeVisible(); + }); + + test('failure before any wipe does not warn about wiped data', async ({ page }) => { + await mockPortalAuth(page); + await setupAndLogin(page, { factoryResetResultAcked: false }); + + // status=1 is Invalid — rejected before the destructive phase started. + await publishToWebsocket('FactoryResetV1', { + keys: ['network'], + result: { + status: 1, + error: 'unknown preserve key', + context: null, + paths: [], + data_wiped: false, + }, + }); + + await expect(page.getByText('Factory Reset Failed', { exact: true })).toBeVisible({ timeout: 5000 }); + await expect(page.getByTestId('factory-reset-data-wiped')).not.toBeVisible(); + }); + + test('unknown status code still shows a result modal', async ({ page }) => { + await mockPortalAuth(page); + await setupAndLogin(page, { factoryResetResultAcked: false }); + + // ODS reports a status it does not know itself as u32::MAX. The result must + // still reach the user instead of being swallowed as "no result". + await publishToWebsocket('FactoryResetV1', { + keys: ['network'], + result: { + status: 4294967295, + error: null, + context: null, + paths: [], + data_wiped: false, + }, + }); + + await expect(page.getByText('Factory Reset Failed', { exact: true })).toBeVisible({ timeout: 5000 }); + }); }); diff --git a/src/ui/tests/fixtures/network-test-harness.ts b/src/ui/tests/fixtures/network-test-harness.ts index 37369b6..c1bca9b 100644 --- a/src/ui/tests/fixtures/network-test-harness.ts +++ b/src/ui/tests/fixtures/network-test-harness.ts @@ -213,8 +213,8 @@ export class NetworkTestHarness { contentType: 'application/json', body: JSON.stringify({ versionInfo: { - required: '>=0.39.0', - current: '0.40.0', + required: '>=0.46.0', + current: '0.46.0', mismatch: false, }, updateValidationStatus: { @@ -604,7 +604,7 @@ export class NetworkTestHarness { 'Access-Control-Allow-Credentials': 'true', }, body: JSON.stringify({ - versionInfo: { required: '>=0.39.0', current: '0.40.0', mismatch: false }, + versionInfo: { required: '>=0.46.0', current: '0.46.0', mismatch: false }, updateValidationStatus: { status: 'valid' }, networkRollbackOccurred: this.networkRollbackOccurred, }), diff --git a/src/ui/tests/network-configuration.spec.ts b/src/ui/tests/network-configuration.spec.ts index 72498de..15a0aa6 100644 --- a/src/ui/tests/network-configuration.spec.ts +++ b/src/ui/tests/network-configuration.spec.ts @@ -338,7 +338,7 @@ test.describe('Network Configuration - Comprehensive E2E Tests', () => { status: 200, contentType: 'application/json', body: JSON.stringify({ - versionInfo: { required: '>=0.39.0', current: '0.40.0', mismatch: false }, + versionInfo: { required: '>=0.46.0', current: '0.46.0', mismatch: false }, updateValidationStatus: { status: 'valid' }, networkRollbackOccurred: healthcheckRollbackStatus, }), @@ -469,7 +469,7 @@ test.describe('Network Configuration - Comprehensive E2E Tests', () => { status: 200, contentType: 'application/json', body: JSON.stringify({ - versionInfo: { required: '>=0.39.0', current: '0.40.0', mismatch: false }, + versionInfo: { required: '>=0.46.0', current: '0.46.0', mismatch: false }, updateValidationStatus: { status: 'valid' }, networkRollbackOccurred: true, }), diff --git a/src/ui/tests/version-mismatch.spec.ts b/src/ui/tests/version-mismatch.spec.ts index 821906d..7849f22 100644 --- a/src/ui/tests/version-mismatch.spec.ts +++ b/src/ui/tests/version-mismatch.spec.ts @@ -13,8 +13,8 @@ test.describe('Version Mismatch', () => { contentType: 'application/json', body: JSON.stringify({ versionInfo: { - required: '>=0.39.0', - current: '0.35.0', + required: '>=0.46.0', + current: '0.45.2', mismatch: true, }, updateValidationStatus: { @@ -30,8 +30,8 @@ test.describe('Version Mismatch', () => { // The version mismatch dialog should appear await expect(page.getByText('omnect-device-service version mismatch')).toBeVisible({ timeout: 10000 }); - await expect(page.getByText('Current version: 0.35.0')).toBeVisible(); - await expect(page.getByText('Required version >=0.39.0')).toBeVisible(); + await expect(page.getByText('Current version: 0.45.2')).toBeVisible(); + await expect(page.getByText('Required version >=0.46.0')).toBeVisible(); await expect(page.getByText('Please consider to update omnect Secure OS')).toBeVisible(); // Dialog should be persistent (no close button, can't dismiss) @@ -50,8 +50,8 @@ test.describe('Version Mismatch', () => { contentType: 'application/json', body: JSON.stringify({ versionInfo: { - required: '>=0.39.0', - current: '0.40.0', + required: '>=0.46.0', + current: '0.46.0', mismatch: false, }, updateValidationStatus: {