summaryrefslogtreecommitdiffhomepage
path: root/desktop
diff options
context:
space:
mode:
authorTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-04-07 13:35:59 +0200
committerSebastian Holmin <sebastian.holmin@mullvad.net>2025-05-28 13:25:31 +0200
commitbe7fb2dae80f05821f13c7fac79ab7bbcbb247b7 (patch)
treeb05568d2b09d881c9ad89aea9c647bdd68f359ca /desktop
parentccc64c7285af4a9179add9219867763e6e13e55c (diff)
downloadmullvadvpn-be7fb2dae80f05821f13c7fac79ab7bbcbb247b7.tar.xz
mullvadvpn-be7fb2dae80f05821f13c7fac79ab7bbcbb247b7.zip
Add AppUpgrade test
Diffstat (limited to 'desktop')
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/mocked/app-upgrade.spec.ts361
1 files changed, 361 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/test/e2e/mocked/app-upgrade.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/mocked/app-upgrade.spec.ts
new file mode 100644
index 0000000000..3d2817ee71
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/test/e2e/mocked/app-upgrade.spec.ts
@@ -0,0 +1,361 @@
+import { expect, test } from '@playwright/test';
+import { Page } from 'playwright';
+
+import { AppUpgradeError, AppUpgradeEvent } from '../../../src/shared/app-upgrade';
+import { IAppVersionInfo } from '../../../src/shared/daemon-rpc-types';
+import { MockedTestUtils, startMockedApp } from './mocked-utils';
+
+let page: Page;
+let util: MockedTestUtils;
+
+test.beforeAll(async () => {
+ ({ page, util } = await startMockedApp());
+
+ await util.sendMockIpcResponse<IAppVersionInfo>({
+ channel: 'upgradeVersion-',
+ response: {
+ supported: true,
+ suggestedIsBeta: false,
+ suggestedUpgrade: {
+ version: '2100.1',
+ changelog: [
+ 'This is a changelog.',
+ 'Each item is on a separate line.',
+ 'There are three items.',
+ ],
+ },
+ },
+ });
+});
+
+test.afterEach(async () => {
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(null);
+ }, 5000);
+ });
+});
+
+test.afterAll(async () => {
+ await page.close();
+});
+
+test('App should navigate to App upgrade view', async () => {
+ await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
+ await util.waitForNavigation(() => page.getByRole('button', { name: 'App info' }).click());
+ await util.waitForNavigation(() =>
+ page.getByRole('button', { name: 'Update available' }).click(),
+ );
+});
+
+test('App should display version and changelog of new upgrade', async () => {
+ const headingText = await page
+ .getByRole('heading', {
+ name: 'Version',
+ })
+ .textContent();
+ expect(headingText).toBe('Version 2100.1');
+
+ const changelogList = page.getByRole('list');
+ const changelogListText = await changelogList.textContent();
+ expect(changelogListText).toEqual(
+ 'This is a changelog.Each item is on a separate line.There are three items.',
+ );
+
+ const changelogListItems = page.getByRole('listitem');
+ const changelogListItemsCount = await changelogListItems.count();
+ expect(changelogListItemsCount).toBe(3);
+});
+
+test('App should start upgrade when clicking Download & install button', async () => {
+ const downloadAndInstallButton = page.getByRole('button', {
+ name: 'Download and install',
+ });
+
+ const appUpgradePromise = util.mockIpcHandle({
+ channel: 'appUpgrade',
+ response: undefined,
+ });
+
+ await downloadAndInstallButton.click();
+
+ // The appUpgrade promise is resolved when its handle has been called.
+ // The handle should be called when the Download & install button is clicked.
+ await appUpgradePromise;
+
+ // Mock that we have started downloading the upgrade
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_DOWNLOAD_STARTED',
+ },
+ });
+
+ await expect(downloadAndInstallButton).toBeHidden();
+});
+
+test('App should show Cancel button after upgrade started', async () => {
+ const cancelButton = page.getByRole('button', {
+ name: 'Cancel',
+ });
+ await expect(cancelButton).toBeVisible();
+ await expect(cancelButton).toBeEnabled();
+});
+
+test('App should show indeterminate download progress after upgrade started', async () => {
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Downloading...')).toBeVisible();
+ await expect(page.getByText('Starting download...')).toBeVisible();
+ await expect(page.getByText('0%')).toBeVisible();
+
+ const downloadProgressBarValue = await page
+ .getByRole('progressbar')
+ .getAttribute('aria-valuenow');
+ expect(downloadProgressBarValue).toEqual('0');
+});
+
+test('App should show download progress after receiving event', async () => {
+ // Mock that we have started downloading the upgrade
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_DOWNLOAD_PROGRESS',
+ progress: 90,
+ server: 'cdn.mullvad.net',
+ timeLeft: 120,
+ },
+ });
+
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Downloading from: cdn.mullvad.net')).toBeVisible();
+ await expect(page.getByText('About 2 minutes remaining...')).toBeVisible();
+
+ const downloadProgressBarValue = await page
+ .getByRole('progressbar')
+ .getAttribute('aria-valuenow');
+ expect(downloadProgressBarValue).toEqual('90');
+});
+
+test('App should cancel upgrade when clicking the Cancel button', async () => {
+ const cancelButton = page.getByRole('button', {
+ name: 'Cancel',
+ });
+
+ const appUpgradeAbortPromise = util.mockIpcHandle({
+ channel: 'appUpgradeAbort',
+ response: undefined,
+ });
+
+ await cancelButton.click();
+
+ // The appUpgradeAbort promise is resolved when its handle has been called.
+ // The handle should be called when the cancel button is clicked.
+ await appUpgradeAbortPromise;
+
+ // After the app upgrade abort RPC is sent we expect to receive an aborted
+ // event.
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_ABORTED',
+ },
+ });
+
+ // The cancel button should be hidden when the upgrade is aborted
+ await expect(cancelButton).toBeHidden();
+
+ // The Download & install button should become visible again
+ const downloadAndInstallButton = page.getByRole('button', {
+ name: 'Download and install',
+ });
+ await expect(downloadAndInstallButton).toBeVisible();
+});
+
+test('App should start upgrade again when clicking Download & install button', async () => {
+ const downloadAndInstallButton = page.getByRole('button', {
+ name: 'Download and install',
+ });
+
+ const appUpgradePromise = util.mockIpcHandle({
+ channel: 'appUpgrade',
+ response: undefined,
+ });
+
+ await downloadAndInstallButton.click();
+
+ // The appUpgrade promise is resolved when its handle has been called.
+ // The handle should be called when the Download & install button is clicked.
+ await appUpgradePromise;
+
+ // Mock that we have started downloading the upgrade
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_DOWNLOAD_STARTED',
+ },
+ });
+
+ await expect(downloadAndInstallButton).toBeHidden();
+});
+
+test('App should show that it is verifying the installer when download is complete', async () => {
+ // Mock that we have started verifying the upgrade
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_VERIFYING_INSTALLER',
+ },
+ });
+
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Verifying installer')).toBeVisible();
+ await expect(page.getByText('Download complete')).toBeVisible();
+
+ const downloadProgressBarValue = await page
+ .getByRole('progressbar')
+ .getAttribute('aria-valuenow');
+ expect(downloadProgressBarValue).toEqual('100');
+});
+
+test('App should show that it has verified the installer when verification is complete', async () => {
+ // Mock that we have verified the upgrade
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_VERIFIED_INSTALLER',
+ },
+ });
+
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Verification successful! Starting installer...')).toBeVisible();
+ await expect(page.getByText('Download complete')).toBeVisible();
+
+ const downloadProgressBarValue = await page
+ .getByRole('progressbar')
+ .getAttribute('aria-valuenow');
+ expect(downloadProgressBarValue).toEqual('100');
+});
+
+test('App should handle failing to automatically start installer', async () => {
+ // Mock the verified upgrade installer path
+ await util.sendMockIpcResponse<IAppVersionInfo>({
+ channel: 'upgradeVersion-',
+ response: {
+ supported: true,
+ suggestedIsBeta: false,
+ suggestedUpgrade: {
+ version: '2100.1',
+ changelog: [
+ 'This is a changelog.',
+ 'Each item is on a separate line.',
+ 'There are three items.',
+ ],
+ verifiedInstallerPath: '/tmp/dummy-path',
+ },
+ },
+ });
+
+ await util.sendMockIpcResponse<AppUpgradeError>({
+ channel: 'app-upgradeError',
+ response: 'START_INSTALLER_AUTOMATIC_FAILED',
+ });
+
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Verification successful! Ready to install.')).toBeVisible();
+
+ const downloadProgressBar = page.getByRole('progressbar');
+ await expect(downloadProgressBar).not.toBeVisible();
+});
+
+test('App should handle failing to manually start installer', async () => {
+ const installUpdateButton = page.getByRole('button', {
+ name: 'Install update',
+ });
+
+ const appUpgradePromise = util.mockIpcHandle({
+ channel: 'appUpgrade',
+ response: undefined,
+ });
+
+ await installUpdateButton.click();
+
+ // The appUpgrade promise is resolved when its handle has been called.
+ // The handle should be called when the Install update button is clicked.
+ await appUpgradePromise;
+
+ // Mock that we have encountered an error the upgrade
+ await util.sendMockIpcResponse<AppUpgradeError>({
+ channel: 'app-upgradeError',
+ response: 'START_INSTALLER_FAILED',
+ });
+
+ await expect(installUpdateButton).not.toBeVisible();
+
+ await expect(
+ page.getByText(
+ 'Could not start the update installer, try downloading it again. If this problem persists, please contact support.',
+ ),
+ ).toBeVisible();
+ const retryDownloadButton = page.getByRole('button', {
+ name: 'Retry download',
+ });
+ await expect(retryDownloadButton).toBeVisible();
+
+ const reportProblemButton = page.getByRole('button', {
+ name: 'Report a problem',
+ });
+ await expect(reportProblemButton).toBeVisible();
+});
+
+test('App should handle retrying downloading the update', async () => {
+ const retryDownloadButton = page.getByRole('button', {
+ name: 'Retry download',
+ });
+
+ const appUpgradePromise = util.mockIpcHandle({
+ channel: 'appUpgrade',
+ response: undefined,
+ });
+
+ await retryDownloadButton.click();
+
+ // The appUpgrade promise is resolved when its handle has been called.
+ // The handle should be called when the Retry download button is clicked.
+ await appUpgradePromise;
+
+ // TODO: Improve by using aria labels
+ await expect(page.getByText('Downloading...')).toBeVisible();
+ await expect(page.getByText('Starting download...')).toBeVisible();
+ await expect(page.getByText('0%')).toBeVisible();
+
+ const downloadProgressBarValue = await page
+ .getByRole('progressbar')
+ .getAttribute('aria-valuenow');
+ expect(downloadProgressBarValue).toEqual('0');
+});
+
+test('App should handle starting installer again if installer was started but did not close the app', async () => {
+ // Mock that we have started the installer. In the real app this event is sent
+ // after a timeout if the GUI is still running after the installer has been started
+ await util.sendMockIpcResponse<AppUpgradeEvent>({
+ channel: 'app-upgradeEvent',
+ response: {
+ type: 'APP_UPGRADE_STATUS_STARTED_INSTALLER',
+ },
+ });
+
+ const installUpdateButton = page.getByRole('button', {
+ name: 'Install update',
+ });
+
+ const appUpgradePromise = util.mockIpcHandle({
+ channel: 'appUpgrade',
+ response: undefined,
+ });
+
+ await installUpdateButton.click();
+
+ // The appUpgrade promise is resolved when its handle has been called.
+ // The handle should be called when the Install update button is clicked.
+ await appUpgradePromise;
+});