Repository: oppia/oppia
Pull request #22190: Fix part of #21646 :Add acceptance test for download any version of the exploration from the history tab
Author: login: Somnath-Mishra
Labels: name: PR: LGTM
Created: 2025-03-20T19:29:03Z | Merged: 2025-03-31T05:56:04Z

## Overview

<!--
READ ME FIRST:
Please answer *all* questions below and check off every point from the Essential Checklist!
If there is no corresponding issue number, fill in N/A where it says [fill_in_number_here] below in 1.
-->

1. This PR fixes part of #21646.
2. This PR does the following: Add acceptance test: 4.8.Download any version of the exploration from the history tab.
- Create an exploration and make several changes to it, saving after each change.
- Go to the history tab.
- Download the current version of the exploration and ensure the correct zip file is downloaded.
- Download an old version of the exploration and ensure the correct zip file is downloaded.

**Debugging Doc:** [[SOLVED] [2025] Debugging Doc - Github CI : Compute Which test to run](https://docs.google.com/document/d/1Y1pvEGOg8KXwIItNCREzQd56Jt_KlsYcu8mCpRctkV8/edit?tab=t.0#heading=h.yz68ilctwvg)
**Github Discussion Thread:** https://github.com/oppia/oppia/discussions/22207 


## Essential Checklist

Please follow the [instructions for making a code change](https://github.com/oppia/oppia/wiki/Make-a-pull-request).

- [x] I have linked the issue that this PR fixes in the "Development" section of the sidebar.
- [x] I have checked the "Files Changed" tab and confirmed that the changes are what I want to make.
- [x] I have written tests for my code.
- [x] The **PR title** starts with "Fix #bugnum: " or "Fix part of #bugnum: ...", followed by a short, clear summary of the changes.
- [x] I have assigned the correct reviewers to this PR (or will leave a comment with the phrase "@{{reviewer_username}} PTAL" if I can't assign them directly).

## Testing doc (for PRs with Beam jobs that modify production server data)

<!--
If this PR affects production server data, please follow
[these instructions](https://github.com/oppia/oppia/wiki/Testing-jobs-and-other-features-on-production#submitting-a-pr-with-a-new-job-or-feature-that-requires-third-party-api)
and link to the job request doc here.

Otherwise, please delete this section.
-->

- [ ] A testing doc has been written: ... (ADD LINK) ...
- [ ] _(To be confirmed by the server admin)_ All jobs in this PR have been verified on a live server, and the PR is safe to merge.

## Proof that changes are correct

![Screenshot from 2025-03-21 00-42-44](https://github.com/user-attachments/assets/64085bc7-216d-4bd8-8e7e-8c6d2b068f8d)


[acceptance_test_for_download_any_version_Exploration_updated_video.webm](https://github.com/user-attachments/assets/a19b7a32-9c55-4770-b4e1-386a0c51ca5f)



<!--
Add before-and-after videos/screenshots of the user-facing interface (including
the browser devtools console) to demonstrate that the changes made in this PR
work correctly. Make sure the actions taken in the before and after videos are
the same. (For changes involving responsiveness or adjustment of UI elements,
this should be a video that gradually resizes the viewport from wide to narrow
and back again.) If this PR is for a developer-facing feature, provide
videos/screenshots of the developer-facing interface instead.

When you make updates to the PR, please update these videos/screenshots as well.
You can drop videos/screenshots from previous versions of the PR.

The above should be done for all PRs, including short ones (e.g. a single-line change).
However, if the changes in your PRs are autogenerated via a script and you cannot
provide proof for the changes then please leave a comment "No proof of changes
needed because {{Reason}}" and remove all the sections below.
-->

#### Proof of changes on desktop with slow/throttled network

<!--
Make sure to properly verify that everything works correctly, and that there are
no weird UI mistakes or other problems. Also, if there are any newly added fields,
try to fill them out and test that different inputs are correctly accepted/rejected.

Throttle the network (to 3G) using the browser Developer Tools (see references below).
There should be no performance or UI issues while the network is slow.

References:
 - Chrome: https://css-tricks.com/throttling-the-network/
 - Firefox: https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor/Throttling
-->

#### Proof of changes on mobile phone

<!--
In some cases this is not needed (e.g. for pages that we do not expect to
support mobile phones, or for backend-only features).

Feel free to use the Developer Tools emulator for this.

References:
 - Chrome: https://developer.chrome.com/docs/devtools/device-mode/
 - Firefox: https://firefox-source-docs.mozilla.org/devtools-user/index.html#responsive-design-mode
-->

#### Proof of changes in Arabic language

<!--
If the PR changes the UI, make sure to add screenshots with the site
language set to Arabic as well (we use Arabic as it is a language written from right to left).
-->

## PR Pointers

- Never force push! If you do, your PR will be closed.
- To reply to reviewers, follow these instructions: https://github.com/oppia/oppia/wiki/Make-a-pull-request#step-5-address-review-comments-until-all-reviewers-approve
- Some e2e tests are flaky, and can fail for reasons unrelated to your PR. We are working on fixing this, but in the meantime, if you need to restart the tests, please check the ["If your build fails" wiki page](https://github.com/oppia/oppia/wiki/If-CI-checks-fail-on-your-PR).
- See the [Code Owner's wiki page](https://github.com/oppia/oppia/wiki/Oppia's-code-owners-and-checks-to-be-carried-out-by-developers) for what code owners will expect.



Diff:
diff --git a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.html b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.html
index 544462bf99ad3..241405d3d1f70 100644
--- a/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.html
+++ b/core/templates/pages/exploration-editor-page/editor-navigation/editor-navigation.component.html
@@ -56,7 +56,7 @@
               <span class="oppia-exploration-editor-tabs-dropdown-inner">Improvements</span>
             </a>
           </li>
-          <li ngbDropdownItem class="dropdown-item oppia-exploration-editor-tabs-dropdown-item" [ngClass]="{'active': getActiveTabName() === 'history'}">
+          <li ngbDropdownItem class="dropdown-item oppia-exploration-editor-tabs-dropdown-item e2e-test-mobile-history-button" [ngClass]="{'active': getActiveTabName() === 'history'}">
             <a href="#" ngbTooltip="History" placement="bottom" (click)="selectHistoryTab()"
                disabled="ExplorationRightsService.isCloned()"
                class="oppia-exploration-editor-tabs-dropdown-element">
diff --git a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.html b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.html
index d1414c18ee659..075b3bd350495 100644
--- a/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.html
+++ b/core/templates/pages/exploration-editor-page/history-tab/history-tab.component.html
@@ -35,7 +35,7 @@ <h3 class="history-card-header-text" tabindex="0">History</h3>
         <span>Displaying {{ startingIndex }}-{{ endIndex }} of {{ totalExplorationVersionMetadata.length }}</span>
       </div>
       <div *ngFor="let metadata of explorationVersionMetadata; index as index" class="history-list-item e2e-test-history-list-item">
-        <div class="history-table-index" [attr.aria-label]="'Version number: '+ metadata.versionNumber" tabindex="0">
+        <div class="history-table-index e2e-test-history-table-index" [attr.aria-label]="'Version number: '+ metadata.versionNumber" tabindex="0">
           {{ metadata.versionNumber }}.
         </div>
         <div class="history-table-message e2e-test-history-table-message" [attr.aria-label]="'Notes: '+ metadata.commitMessage" tabindex="0">
@@ -49,7 +49,7 @@ <h3 class="history-card-header-text" tabindex="0">History</h3>
         </div>
         <div class="history-table-options e2e-test-history-list-options">
           <div ngbDropdown class="dropdown">
-            <button ngbDropdownToggle class="history-table-option" type="button" id="{{ 'dropdownMenuButton-' + index }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" tabindex="0" role="button" aria-label="options">
+            <button ngbDropdownToggle class="history-table-option e2e-test-history-table-option" type="button" id="{{ 'dropdownMenuButton-' + index }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" tabindex="0" role="button" aria-label="options">
               <i class="fa fa-ellipsis-v"></i>
             </button>
             <div ngbDropdownMenu class="dropdown-menu dropdown-menu-left" [attr.aria-label]="'dropdownMenuButton-' + index">
@@ -59,7 +59,7 @@ <h3 class="history-card-header-text" tabindex="0">History</h3>
                  (click)="showCheckRevertExplorationModal(metadata.versionNumber)">
                 Revert
               </a>
-              <a ngbDropdownItem class="dropdown-item" href="#" (click)="downloadExplorationWithVersion(metadata.versionNumber)"
+              <a ngbDropdownItem class="dropdown-item e2e-test-download-exploration" href="#" (click)="downloadExplorationWithVersion(metadata.versionNumber)"
                  title="Download exploration as a zip file.">
                 Download
               </a>
diff --git a/core/tests/ci-test-suite-configs/acceptance.json b/core/tests/ci-test-suite-configs/acceptance.json
index 043034bf94153..c3ac3486bf148 100644
--- a/core/tests/ci-test-suite-configs/acceptance.json
+++ b/core/tests/ci-test-suite-configs/acceptance.json
@@ -48,6 +48,10 @@
       "name": "exploration-editor/save-draft-publish-and-discard-the-changes",
       "module": "core/tests/puppeteer-acceptance-tests/specs/exploration-editor/save-draft-publish-and-discard-the-changes.spec.ts"
     },
+    {
+      "name": "exploration-editor/download-any-version-exploration",
+      "module": "core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts"
+    },
     {
       "name": "exploration-editor/create-exploration-with-multiple-states-answer-group-hint-solution",
       "module": "core/tests/puppeteer-acceptance-tests/specs/exploration-editor/create-exploration-with-multiple-states-answer-group-hint-solution.spec.ts"
diff --git a/core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts b/core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
new file mode 100644
index 0000000000000..727467ccf9777
--- /dev/null
+++ b/core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
@@ -0,0 +1,87 @@
+// Copyright 2025 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Acceptance Test for download any version of the exploration from the history tab.
+ */
+
+import {UserFactory} from '../../utilities/common/user-factory';
+import testConstants from '../../utilities/common/test-constants';
+import {ExplorationEditor} from '../../utilities/user/exploration-editor';
+
+const DEFAULT_SPEC_TIMEOUT_MSECS = testConstants.DEFAULT_SPEC_TIMEOUT_MSECS;
+
+enum INTERACTION_TYPES {
+  END_EXPLORATION = 'End Exploration',
+}
+
+describe('Exploration Creator', function () {
+  let explorationEditor: ExplorationEditor;
+
+  beforeAll(async function () {
+    explorationEditor = await UserFactory.createNewUser(
+      'explorationEditor',
+      'exploration_editor@example.com'
+    );
+    // Navigate to the creator dashboard and create a new exploration.
+    await explorationEditor.navigateToCreatorDashboardPage();
+    await explorationEditor.navigateToExplorationEditorPage();
+    await explorationEditor.dismissWelcomeModal();
+
+    await explorationEditor.createMinimalExploration(
+      'Content',
+      INTERACTION_TYPES.END_EXPLORATION
+    );
+
+    await explorationEditor.saveExplorationDraft();
+
+    await explorationEditor.updateCardContent('Modified version 3 ');
+    await explorationEditor.saveExplorationDraft();
+
+    await explorationEditor.updateCardContent('Modified version 4 ');
+    await explorationEditor.saveExplorationDraft();
+
+    await explorationEditor.updateCardContent('Modified version 5 ');
+    await explorationEditor.saveExplorationDraft();
+  }, DEFAULT_SPEC_TIMEOUT_MSECS);
+
+  it(
+    'should download any version of Exploration',
+    async function () {
+      // Navigate to the history tab, .
+      await explorationEditor.navigateToHistoryTab();
+      // Before publishing, the Exploration zip file name would be
+      // oppia-unpublished_exploration-v{versionNumber}.zip or
+      // oppia-unpublished_exploration-v{versionNumber} (numberOfDownloadsSameFile).zip.
+      await explorationEditor.downloadExploration(5, false);
+      await explorationEditor.downloadExploration(2, false);
+      // Publish Exploration.
+      await explorationEditor.publishExplorationWithMetadata(
+        'Publish with an interaction',
+        'This is the goal of exploration.',
+        'Algebra'
+      );
+      // After publishing, the Exploration zip file name would be
+      // oppia-{explorationTitle}-v{versionNumber}.zip or
+      // oppia-{explorationTitle}-v{versionNumber} (numberOfDownloadSameFile).zip .
+      await explorationEditor.downloadExploration(5, true);
+      await explorationEditor.downloadExploration(2, true);
+    },
+    DEFAULT_SPEC_TIMEOUT_MSECS
+  );
+
+  afterAll(async function () {
+    await UserFactory.closeAllBrowsers();
+  });
+});
diff --git a/core/tests/puppeteer-acceptance-tests/utilities/common/puppeteer-utils.ts b/core/tests/puppeteer-acceptance-tests/utilities/common/puppeteer-utils.ts
index a518e6ddb2480..9b19bae065764 100644
--- a/core/tests/puppeteer-acceptance-tests/utilities/common/puppeteer-utils.ts
+++ b/core/tests/puppeteer-acceptance-tests/utilities/common/puppeteer-utils.ts
@@ -191,6 +191,21 @@ export class BaseUser {
           });
         }
 
+        // Set up Download Folder.
+        const downloadDir = testConstants.TEST_DOWNLOAD_DIR;
+
+        // Ensure the folder exists.
+        if (!fs.existsSync(downloadDir)) {
+          fs.mkdirSync(downloadDir, {recursive: true});
+        }
+
+        // Enable download behavior using Chrome DevTools Protocol (CDP).
+        const client = await this.page.target().createCDPSession();
+        await client.send('Page.setDownloadBehavior', {
+          behavior: 'allow',
+          downloadPath: downloadDir,
+        });
+
         this.page.on('dialog', async dialog => {
           const alertText = dialog.message();
           if (acceptedBrowserAlerts.includes(alertText)) {
diff --git a/core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts b/core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts
index 825bea39b8e80..2954609564d6a 100644
--- a/core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts
+++ b/core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts
@@ -246,4 +246,5 @@ export default {
   TEST_SCREENSHOT_DIR: path.resolve(
     '../oppia_full_stack_test_failure_screenshots/acceptance'
   ),
+  TEST_DOWNLOAD_DIR: path.resolve('../oppia_acceptance_test_downloads'),
 };
diff --git a/core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts b/core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
index 51a9c2af5daa8..2f9f1a24f1cfd 100644
--- a/core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
+++ b/core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
@@ -21,6 +21,8 @@ import {BaseUser} from '../common/puppeteer-utils';
 import testConstants from '../common/test-constants';
 import {showMessage} from '../common/show-message';
 import {error} from 'console';
+import fs from 'fs';
+import path from 'path';
 
 const creatorDashboardPage = testConstants.URLs.CreatorDashboard;
 const baseUrl = testConstants.URLs.BaseURL;
@@ -219,12 +221,24 @@ const stayAnonymousCheckbox = '.e2e-test-stay-anonymous-checkbox';
 const responseTextareaSelector = '.e2e-test-feedback-response-textarea';
 const sendButtonSelector = '.e2e-test-oppia-feedback-response-send-btn';
 const errorSavingExplorationModal = '.e2e-test-discard-lost-changes-button';
+const historyTabButton = '.e2e-test-history-tab';
+const historyListContent = '.e2e-test-history-list-item';
+const mobileHistoryTabButton = '.e2e-test-mobile-history-button';
 const totalPlaysSelector = '.e2e-test-oppia-total-plays';
 const numberOfOpenFeedbacksSelector = '.e2e-test-oppia-open-feedback';
 const avarageRatingSelector = '.e2e-test-oppia-average-rating';
 const usersCountInRatingSelector = '.e2e-test-oppia-total-users';
+const historyTableIndex = '.history-table-index';
+const historyListOptions = '.e2e-test-history-list-options';
+const downloadExplorationButton =
+  'a.dropdown-item.e2e-test-download-exploration';
 
+const downloadPath = testConstants.TEST_DOWNLOAD_DIR;
 const LABEL_FOR_SAVE_DESTINATION_BUTTON = ' Save Destination ';
+const UNPUBLISHED_EXPLORATION_ZIP_FILE_PREFIX =
+  'oppia-unpublished_exploration-v';
+const PUBLISHED_EXPLORATION_ZIP_FILE_PREFIX =
+  'oppia-Publishwithaninteraction-v';
 export class ExplorationEditor extends BaseUser {
   /**
    * Function to navigate to creator dashboard page.
@@ -1549,6 +1563,132 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? PUBLISHED_EXPLORATION_ZIP_FILE_PREFIX
+      : UNPUBLISHED_EXPLORATION_ZIP_FILE_PREFIX;
+
+    const files = fs.readdirSync(downloadPath);
+    return files.filter(file =>
+      file.match(new RegExp(`^${filePrefix}${version}( \\(\\d+\\))?\\.zip$`))
+    );
+  }
+
+  /**
+   * Generates the expected filename based on the existing count.
+   * @param {number} version - The version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @param {number} fileCount - The number of existing files.
+   * @returns {string} - The expected filename.
+   */
+  getExpectedFileName(
+    version: number,
+    isPublished: boolean,
+    fileCount: number
+  ): string {
+    const filePrefix = isPublished
+      ? PUBLISHED_EXPLORATION_ZIP_FILE_PREFIX
+      : UNPUBLISHED_EXPLORATION_ZIP_FILE_PREFIX;
+    return fileCount === 0
+      ? `${filePrefix}${version}.zip`
+      : `${filePrefix}${version} (${fileCount}).zip`;
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$(historyTableIndex);
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      // Check whether the current exploration version matches the given explorationVersion.
+      if (parseInt(versionText, 10) === explorationVersion) {
+        // Count existing files with same name before downloading.
+        const existingFiles = await this.getExistingVersionFiles(
+          explorationVersion,
+          isExplorationPublished
+        );
+        const nextNumber = existingFiles.length;
+        const expectedFileName = this.getExpectedFileName(
+          explorationVersion,
+          isExplorationPublished,
+          nextNumber
+        );
+
+        const dropdownButton = await historyItem.$(historyListOptions);
+        await this.page.evaluate(el => el.click(), dropdownButton);
+        await this.page.waitForTimeout(1000);
+        const downloadButton = await historyItem.$(downloadExplorationButton);
+        await this.page.evaluate(el => el.click(), downloadButton);
+        await this.page.waitForTimeout(5000);
+        const downloadedFile =
+          await this.waitForExplorationDownload(expectedFileName);
+        if (downloadedFile) {
+          showMessage(`${downloadedFile} file is successfully downloaded`);
+          return;
+        } else {
+          throw new Error(
+            `Download failed for Exploration version: ${explorationVersion}`
+          );
+        }
+      }
+    }
+  }
+
+  /**
+   * Waits for a downloaded file to appear and cleans up only the new one.
+   * @param {string} expectedFileName - The expected file name.
+   * @returns {Promise<string | null>} - The verified file name or null if not found.
+   */
+  async waitForExplorationDownload(
+    expectedFileName: string
+  ): Promise<string | null> {
+    // Wait for network to be idle after triggering the download.
+    await this.page.waitForNetworkIdle();
+    const files = fs.readdirSync(downloadPath);
+    const downloadedFile =
+      files.find(file => file === expectedFileName) || null;
+    if (
+      downloadedFile &&
+      fs.existsSync(path.join(downloadPath, downloadedFile))
+    ) {
+      fs.unlinkSync(path.join(downloadPath, downloadedFile));
+    }
+    return downloadedFile;
+  }
+
   /**
    * Function to navigate to the translations tab.
    */
diff --git a/core/tests/test-dependencies/typescript-ast-utilities.ts b/core/tests/test-dependencies/typescript-ast-utilities.ts
index 5c4b86fe66b06..b16398ffd935f 100644
--- a/core/tests/test-dependencies/typescript-ast-utilities.ts
+++ b/core/tests/test-dependencies/typescript-ast-utilities.ts
@@ -52,7 +52,7 @@ type ResolvedModuleWithFailedLookupLocations =
 /**
  * List of types that are defined in node.
  */
-const NODE_TYPE_ROOTS = ['fs', 'console', 'path', 'child_process'];
+const NODE_TYPE_ROOTS = ['fs', 'console', 'path', 'child_process', 'os'];
 
 /**
  * Checks if a module is a node module.
diff --git a/scripts/clean.py b/scripts/clean.py
index fab501eafdfe8..01d17ed476cb7 100644
--- a/scripts/clean.py
+++ b/scripts/clean.py
@@ -27,6 +27,8 @@
     CURR_DIR, '..', 'oppia_full_stack_test_video_recordings')
 FULL_STACK_TEST_SCREENSHOT_DIR = os.path.join(
     CURR_DIR, '..', 'oppia_full_stack_test_screenshots')
+FULL_STACK_TEST_DOWNLOAD_DIR = os.path.join(
+    CURR_DIR, '..', 'oppia_acceptance_test_downloads')
 
 _PARSER = argparse.ArgumentParser(
     description="""
@@ -64,6 +66,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:
     delete_directory_tree(OPPIA_TOOLS_DIR)
     delete_directory_tree(FULL_STACK_TEST_VIDEO_RECORDING_DIR)
     delete_directory_tree(FULL_STACK_TEST_SCREENSHOT_DIR)
+    delete_directory_tree(FULL_STACK_TEST_DOWNLOAD_DIR)
     delete_directory_tree('node_modules/')
     delete_directory_tree('third_party/')
     delete_directory_tree('build/')
diff --git a/scripts/clean_test.py b/scripts/clean_test.py
index 1d2f1276b1e39..9ec38435107f6 100644
--- a/scripts/clean_test.py
+++ b/scripts/clean_test.py
@@ -109,7 +109,7 @@ def test_function_calls(self) -> None:
             'delete_file_is_called': 0
         }
         expected_check_function_calls = {
-            'delete_directory_tree_is_called': 11,
+            'delete_directory_tree_is_called': 12,
             'delete_file_is_called': 4
         }
         def mock_delete_dir(unused_path: str) -> None:
diff --git a/scripts/common.py b/scripts/common.py
index 077db32e6d96b..ee44c5a177fd9 100644
--- a/scripts/common.py
+++ b/scripts/common.py
@@ -217,6 +217,7 @@
     'exploration-editor/load-complete-and-restart-exploration-preview',
     'exploration-editor/publish-the-exploration-with-an-interaction',
     'exploration-editor/save-draft-publish-and-discard-the-changes',
+    'exploration-editor/download-any-version-exploration',
     'exploration-editor/verify-statistics-and-previous-explorations',
     'exploration-editor/create-exploration-with-multiple-states'
     '-answer-group-hint-solution',


Reviews:
user: login: seanlip
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-23T18:28:26Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T18:33:58Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T16:46:53Z
user: login: Vir-8
body: LGTM for codeowner files.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T06:16:20Z
user: login: imchristie
body: Left some comments. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T10:08:45Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:02:31Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:07:44Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:14:13Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:19:57Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T16:22:03Z
user: login: imchristie
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T08:52:25Z
user: login: seanlip
body: Just one note, otherwise looks good, thanks.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T18:48:45Z
user: login: Somnath-Mishra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-29T16:28:36Z
user: login: seanlip
body: Thanks, LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T05:15:15Z

Review comments:
diff_hunk: @@ -1549,6 +1556,91 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      if (parseInt(versionText, 10) === explorationVersion) {
+        const dropdownButton = await historyItem.$(
+          '.e2e-test-history-list-options'
+        );
+        await dropdownButton?.click();
+        await this.page.waitForTimeout(1000);
+        const downloadButton = await historyItem.$(
+          'a.dropdown-item.e2e-test-download-exploration'
+        );
+        await this.page.evaluate(el => el.click(), downloadButton);
+        await this.page.waitForTimeout(5000);
+        const downloadedFile = await this.waitForExplorationDownload(
+          explorationVersion,
+          isExplorationPublished
+        );
+        if (downloadedFile) {
+          showMessage(`${downloadedFile} file is successfully downloaded`);
+          return;
+        } else {
+          throw new Error(
+            `Download failed for Exploration version: ${explorationVersion}`
+          );
+        }
+      }
+    }
+  }
+
+  /**
+   * Waits for a Exploration zip file to be downloaded and verifies its name.
+   * @param {number} expectedVersion - The expected version number in the file name.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   * @returns {Promise<string | null>} - Returns the file name if correct, otherwise null.
+   */
+  async waitForExplorationDownload(
+    expectedVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<string | null> {
+    // Wait for network to be idle after triggering the download.
+    await this.page.waitForNetworkIdle();
+    const files = fs.readdirSync(downloadPath);
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: seanlip
body: I am a bit worried that your test is reading the developer's entire Downloads folder. Generally tests related to Oppia shouldn't reach "outside" Oppia. Could you isolate the test a bit more, maybe use Downloads/oppia-acceptance-test-downloads or something like that, and only look at that subfolder (which is unlikely to contain other stuff)?

Also given that you're working with a general filesystem that might not get cleaned up, you might need to also properly handle the case where a file of the given name already exists.

Also consider having your tests clean up after themselves. E.g. when downloading any file, store the filepath in a list. At the end of the test, delete all files in the list.

Just some things to consider.
created_at: 2025-03-23T18:26:18Z
author_association: MEMBER
diff_hunk: @@ -1549,6 +1556,91 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      if (parseInt(versionText, 10) === explorationVersion) {
+        const dropdownButton = await historyItem.$(
+          '.e2e-test-history-list-options'
+        );
+        await dropdownButton?.click();
+        await this.page.waitForTimeout(1000);
+        const downloadButton = await historyItem.$(
+          'a.dropdown-item.e2e-test-download-exploration'
+        );
+        await this.page.evaluate(el => el.click(), downloadButton);
+        await this.page.waitForTimeout(5000);
+        const downloadedFile = await this.waitForExplorationDownload(
+          explorationVersion,
+          isExplorationPublished
+        );
+        if (downloadedFile) {
+          showMessage(`${downloadedFile} file is successfully downloaded`);
+          return;
+        } else {
+          throw new Error(
+            `Download failed for Exploration version: ${explorationVersion}`
+          );
+        }
+      }
+    }
+  }
+
+  /**
+   * Waits for a Exploration zip file to be downloaded and verifies its name.
+   * @param {number} expectedVersion - The expected version number in the file name.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   * @returns {Promise<string | null>} - Returns the file name if correct, otherwise null.
+   */
+  async waitForExplorationDownload(
+    expectedVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<string | null> {
+    // Wait for network to be idle after triggering the download.
+    await this.page.waitForNetworkIdle();
+    const files = fs.readdirSync(downloadPath);
+    const filePrefix = isExplorationPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+    const downloadedFile =
+      files.find(file =>
+        file.match(
+          new RegExp(`^${filePrefix}${expectedVersion}( \\(\\d+\\))?\\.zip$`)
+        )
+      ) || null;
+    if (downloadedFile) {
+      fs.unlinkSync(path.join(downloadPath, downloadedFile));
+    }
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: Somnath-Mishra
body: @seanlip 
Here, I clean the files that I downloaded.
created_at: 2025-03-23T18:32:54Z
author_association: COLLABORATOR
diff_hunk: @@ -1549,6 +1556,91 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      if (parseInt(versionText, 10) === explorationVersion) {
+        const dropdownButton = await historyItem.$(
+          '.e2e-test-history-list-options'
+        );
+        await dropdownButton?.click();
+        await this.page.waitForTimeout(1000);
+        const downloadButton = await historyItem.$(
+          'a.dropdown-item.e2e-test-download-exploration'
+        );
+        await this.page.evaluate(el => el.click(), downloadButton);
+        await this.page.waitForTimeout(5000);
+        const downloadedFile = await this.waitForExplorationDownload(
+          explorationVersion,
+          isExplorationPublished
+        );
+        if (downloadedFile) {
+          showMessage(`${downloadedFile} file is successfully downloaded`);
+          return;
+        } else {
+          throw new Error(
+            `Download failed for Exploration version: ${explorationVersion}`
+          );
+        }
+      }
+    }
+  }
+
+  /**
+   * Waits for a Exploration zip file to be downloaded and verifies its name.
+   * @param {number} expectedVersion - The expected version number in the file name.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   * @returns {Promise<string | null>} - Returns the file name if correct, otherwise null.
+   */
+  async waitForExplorationDownload(
+    expectedVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<string | null> {
+    // Wait for network to be idle after triggering the download.
+    await this.page.waitForNetworkIdle();
+    const files = fs.readdirSync(downloadPath);
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: Somnath-Mishra
body: Ok. Done.
created_at: 2025-03-24T16:46:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,87 @@
+// Copyright 2025 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Acceptance Test for download any version of the exploration from the history tab..
path: core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
user: login: imchristie
body: double period. 
created_at: 2025-03-27T09:56:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,87 @@
+// Copyright 2025 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Acceptance Test for download any version of the exploration from the history tab..
+ */
+
+import {UserFactory} from '../../utilities/common/user-factory';
+import testConstants from '../../utilities/common/test-constants';
+import {ExplorationEditor} from '../../utilities/user/exploration-editor';
+
+const DEFAULT_SPEC_TIMEOUT_MSECS = testConstants.DEFAULT_SPEC_TIMEOUT_MSECS;
+
+enum INTERACTION_TYPES {
+  END_EXPLORATION = 'End Exploration',
+}
+
+describe('Exploration Creator', function () {
+  let explorationEditor: ExplorationEditor;
+
+  beforeAll(async function () {
+    explorationEditor = await UserFactory.createNewUser(
+      'explorationEditor',
+      'exploration_editor@example.com'
+    );
+    // Navigate to the creator dashboard and create a new exploration.
+    await explorationEditor.navigateToCreatorDashboardPage();
+    await explorationEditor.navigateToExplorationEditorPage();
+    await explorationEditor.dismissWelcomeModal();
+
+    await explorationEditor.createMinimalExploration(
+      'Content',
+      INTERACTION_TYPES.END_EXPLORATION
+    );
+
+    await explorationEditor.saveExplorationDraft();
+
+    await explorationEditor.updateCardContent('Modified version 3 ');
path: core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
user: login: imchristie
body: What about version 1 and 2?
created_at: 2025-03-27T09:57:11Z
author_association: MEMBER
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: imchristie
body: Since you are using the same two strings for this function and the function below, store the string as const for consistency. 
created_at: 2025-03-27T10:01:02Z
author_association: MEMBER
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+
+    const files = fs.readdirSync(downloadPath);
+    return files.filter(file =>
+      file.match(new RegExp(`^${filePrefix}${version}( \\(\\d+\\))?\\.zip$`))
+    );
+  }
+
+  /**
+   * Generates the expected filename based on the existing count.
+   * @param {number} version - The version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @param {number} fileCount - The number of existing files.
+   * @returns {string} - The expected filename.
+   */
+  getExpectedFileName(
+    version: number,
+    isPublished: boolean,
+    fileCount: number
+  ): string {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+    return fileCount === 0
+      ? `${filePrefix}${version}.zip`
+      : `${filePrefix}${version} (${fileCount}).zip`;
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: imchristie
body: Store all html elements as const. 
created_at: 2025-03-27T10:05:55Z
author_association: MEMBER
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+
+    const files = fs.readdirSync(downloadPath);
+    return files.filter(file =>
+      file.match(new RegExp(`^${filePrefix}${version}( \\(\\d+\\))?\\.zip$`))
+    );
+  }
+
+  /**
+   * Generates the expected filename based on the existing count.
+   * @param {number} version - The version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @param {number} fileCount - The number of existing files.
+   * @returns {string} - The expected filename.
+   */
+  getExpectedFileName(
+    version: number,
+    isPublished: boolean,
+    fileCount: number
+  ): string {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+    return fileCount === 0
+      ? `${filePrefix}${version}.zip`
+      : `${filePrefix}${version} (${fileCount}).zip`;
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      if (parseInt(versionText, 10) === explorationVersion) {
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: imchristie
body: Could you add more comments to explain what it does in this if else?
created_at: 2025-03-27T10:07:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,87 @@
+// Copyright 2025 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Acceptance Test for download any version of the exploration from the history tab..
+ */
+
+import {UserFactory} from '../../utilities/common/user-factory';
+import testConstants from '../../utilities/common/test-constants';
+import {ExplorationEditor} from '../../utilities/user/exploration-editor';
+
+const DEFAULT_SPEC_TIMEOUT_MSECS = testConstants.DEFAULT_SPEC_TIMEOUT_MSECS;
+
+enum INTERACTION_TYPES {
+  END_EXPLORATION = 'End Exploration',
+}
+
+describe('Exploration Creator', function () {
+  let explorationEditor: ExplorationEditor;
+
+  beforeAll(async function () {
+    explorationEditor = await UserFactory.createNewUser(
+      'explorationEditor',
+      'exploration_editor@example.com'
+    );
+    // Navigate to the creator dashboard and create a new exploration.
+    await explorationEditor.navigateToCreatorDashboardPage();
+    await explorationEditor.navigateToExplorationEditorPage();
+    await explorationEditor.dismissWelcomeModal();
+
+    await explorationEditor.createMinimalExploration(
+      'Content',
+      INTERACTION_TYPES.END_EXPLORATION
+    );
+
+    await explorationEditor.saveExplorationDraft();
+
+    await explorationEditor.updateCardContent('Modified version 3 ');
path: core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
user: login: Somnath-Mishra
body: ```
   // This creates version 1 and version 2
   // version 1 --> set card content "Content"
   // version 2 --> set Interaction
    await explorationEditor.createMinimalExploration(
      'Content',
      INTERACTION_TYPES.END_EXPLORATION
    );

    await explorationEditor.saveExplorationDraft();
```
created_at: 2025-03-27T16:02:31Z
author_association: COLLABORATOR
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: Somnath-Mishra
body: Ok. Done
created_at: 2025-03-27T16:07:44Z
author_association: COLLABORATOR
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+
+    const files = fs.readdirSync(downloadPath);
+    return files.filter(file =>
+      file.match(new RegExp(`^${filePrefix}${version}( \\(\\d+\\))?\\.zip$`))
+    );
+  }
+
+  /**
+   * Generates the expected filename based on the existing count.
+   * @param {number} version - The version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @param {number} fileCount - The number of existing files.
+   * @returns {string} - The expected filename.
+   */
+  getExpectedFileName(
+    version: number,
+    isPublished: boolean,
+    fileCount: number
+  ): string {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+    return fileCount === 0
+      ? `${filePrefix}${version}.zip`
+      : `${filePrefix}${version} (${fileCount}).zip`;
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: Somnath-Mishra
body: Ok. Done.
created_at: 2025-03-27T16:14:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1549,6 +1555,135 @@ export class ExplorationEditor extends BaseUser {
     await this.page.waitForNavigation();
   }
 
+  /**
+   * Function to navigate to the history tab.
+   */
+  async navigateToHistoryTab(): Promise<void> {
+    if (this.isViewportAtMobileWidth()) {
+      await this.clickOn(mobileNavbarDropdown);
+      await this.page.waitForSelector(mobileNavbarPane);
+      await this.clickOn(mobileHistoryTabButton);
+    } else {
+      await this.clickOn(historyTabButton);
+    }
+  }
+
+  /**
+   * Gets the list of existing files for the given version.
+   * @param {number} version - The expected version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @returns {string[]} - List of matching file names.
+   */
+  async getExistingVersionFiles(
+    version: number,
+    isPublished: boolean
+  ): Promise<string[]> {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+
+    const files = fs.readdirSync(downloadPath);
+    return files.filter(file =>
+      file.match(new RegExp(`^${filePrefix}${version}( \\(\\d+\\))?\\.zip$`))
+    );
+  }
+
+  /**
+   * Generates the expected filename based on the existing count.
+   * @param {number} version - The version number.
+   * @param {boolean} isPublished - Whether the exploration is published.
+   * @param {number} fileCount - The number of existing files.
+   * @returns {string} - The expected filename.
+   */
+  getExpectedFileName(
+    version: number,
+    isPublished: boolean,
+    fileCount: number
+  ): string {
+    const filePrefix = isPublished
+      ? 'oppia-Publishwithaninteraction-v'
+      : 'oppia-unpublished_exploration-v';
+    return fileCount === 0
+      ? `${filePrefix}${version}.zip`
+      : `${filePrefix}${version} (${fileCount}).zip`;
+  }
+
+  /**
+   * Function to download a specific version of Exploration.
+   * @param {number} explorationVersion - The version of the exploration to download.
+   * @param {boolean} isExplorationPublished - Whether the Exploration is published.
+   */
+  async downloadExploration(
+    explorationVersion: number,
+    isExplorationPublished: boolean
+  ): Promise<void> {
+    const historyItems = await this.page.$$(historyListContent);
+    for (const historyItem of historyItems) {
+      const versionNumberElement = await historyItem.$('.history-table-index');
+      const versionText = await this.page.evaluate(
+        element => element.textContent,
+        versionNumberElement
+      );
+
+      if (parseInt(versionText, 10) === explorationVersion) {
path: core/tests/puppeteer-acceptance-tests/utilities/user/exploration-editor.ts
user: login: Somnath-Mishra
body: Ok. Done.
created_at: 2025-03-27T16:19:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,87 @@
+// Copyright 2025 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Acceptance Test for download any version of the exploration from the history tab..
path: core/tests/puppeteer-acceptance-tests/specs/exploration-editor/download-any-version-exploration.spec.ts
user: login: Somnath-Mishra
body: Ok. updated.
created_at: 2025-03-27T16:22:03Z
author_association: COLLABORATOR
diff_hunk: @@ -246,4 +247,8 @@ export default {
   TEST_SCREENSHOT_DIR: path.resolve(
     '../oppia_full_stack_test_failure_screenshots/acceptance'
   ),
+  TEST_DOWNLOAD_DIR: path.join(
+    os.homedir(),
+    'Downloads/oppia-acceptance-test-downloads'
path: core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts
user: login: seanlip
body: Maybe it is better to use ../ like the above two, for consistency with what we already do? And underscores as well in the folder name.

In general, try to make your approaches consistent with the surrounding code. Fewer cases to handle that way, so it helps with maintainability.
created_at: 2025-03-28T18:47:07Z
author_association: MEMBER
diff_hunk: @@ -246,4 +247,8 @@ export default {
   TEST_SCREENSHOT_DIR: path.resolve(
     '../oppia_full_stack_test_failure_screenshots/acceptance'
   ),
+  TEST_DOWNLOAD_DIR: path.join(
+    os.homedir(),
+    'Downloads/oppia-acceptance-test-downloads'
path: core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts
user: login: Somnath-Mishra
body: Ok. Done.
created_at: 2025-03-29T16:28:36Z
author_association: COLLABORATOR

PR comments:
user: login: oppiabot[bot]
created_at: 2025-03-20T19:29:05Z
body: Hi @Somnath-Mishra, can you complete the following:
1. The body of this PR is missing the required description, please update the body with a description of what this PR does.
Thanks!
author_association: NONE
user: login: oppiabot[bot]
created_at: 2025-03-20T19:29:15Z
body: Hi @Somnath-Mishra please assign the required reviewer(s) for this PR. Thanks!
author_association: NONE
user: login: Somnath-Mishra
created_at: 2025-03-22T11:18:27Z
body: @imchristie @Vir-8 PTAL.
Thanks.
author_association: COLLABORATOR
user: login: Somnath-Mishra
created_at: 2025-03-24T16:47:11Z
body: @seanlip PTAL
Thanks
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-24T16:47:26Z
body: Assigned reviewers: @seanlip. Thank you!
author_association: NONE
user: login: oppiabot[bot]
created_at: 2025-03-27T06:19:23Z
body: Unassigning @Vir-8 since they have already approved the PR.
author_association: NONE
user: login: Somnath-Mishra
created_at: 2025-03-27T16:27:46Z
body: @imchristie PTAL
Thanks.
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-27T16:27:57Z
body: Assigned reviewers: @imchristie. Thank you!
author_association: NONE
user: login: Somnath-Mishra
created_at: 2025-03-27T17:13:51Z
body: CI failure because of flake https://github.com/oppia/oppia/issues/21838
author_association: COLLABORATOR
user: login: oppiabot[bot]
created_at: 2025-03-28T08:55:27Z
body: Unassigning @imchristie since they have already approved the PR.
author_association: NONE
user: login: Somnath-Mishra
created_at: 2025-03-29T16:29:31Z
body: @seanlip PTAL
Thanks.
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-03-29T16:29:41Z
body: Assigned reviewers: @seanlip. Thank you!
author_association: NONE
user: login: oppiabot[bot]
created_at: 2025-03-31T05:18:17Z
body: Unassigning @seanlip since they have already approved the PR.
author_association: NONE
user: login: oppiabot[bot]
created_at: 2025-03-31T05:18:21Z
body: Hi @Somnath-Mishra, this PR is ready to be merged. Please address any remaining comments prior to merging, and feel free to ask someone to merge your PR once the CI checks pass and you're happy with it. Thanks!
author_association: NONE
user: login: oppia-github-app[bot]
created_at: 2025-03-31T07:04:17Z
body: Congratulations! This PR has been deployed to the test server. You can test it out at [this link](https://www.oppiatestserver.org/).
author_association: NONE