diff --git a/.github/workflows/e2e-cross-version.yml b/.github/workflows/e2e-cross-version.yml
index 440ce2340c22c91025a43503cb32a36d37cbb552..38a3fc0b9a382ad449a9277c757a335ece131bcc 100644
--- a/.github/workflows/e2e-cross-version.yml
+++ b/.github/workflows/e2e-cross-version.yml
@@ -11,6 +11,8 @@ jobs:
     strategy:
       matrix:
         version: [
+          # Major OSS upgrade
+          { source: v0.42.2, target: v0.48.7 },
           # OSS upgrade
           { source: v0.41.3.1, target: v0.42.2 },
           # EE upgrade
diff --git a/e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js b/e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cc385a0ee32e1a396ca059921595788096e01bd
--- /dev/null
+++ b/e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js
@@ -0,0 +1,175 @@
+export function parseVersionString(versionString) {
+  if (typeof versionString === "undefined") {
+    return []; // Return empty array if versionString is undefined
+  }
+
+  const segments = versionString.match(/\d+/g);
+  if (!segments) {
+    return { version: versionString }; // Return versionString if no segments found
+  }
+
+  const segmentInts = segments.map(segment => parseInt(segment, 10));
+  const editionIndex = segmentInts[0];
+
+  let edition = null;
+  if (editionIndex === 1) {
+    edition = "ee";
+  } else if (editionIndex === 0) {
+    edition = "oss";
+  }
+
+  return {
+    version: versionString,
+    edition: edition,
+    majorVersion: segmentInts[1],
+    minorVersion: segmentInts[2],
+    patchVersion: segmentInts[3],
+  };
+}
+
+// Versions in which a breaking GUI change was introduced. For example, version
+// 32 added a "What will you use Metabase for?" stage in the initial setup.
+
+// Change: Adds "What will you use Metabase for?" stage in the initial setup.
+// Git sha: d88d32e5e021ad4f47b5d740b78df73945e8ff82
+// Date: 2024-02-08
+// Author: npretto
+const metabasePurposeVersion = 49;
+
+// Change: Question save modal logic changes required different cypress logic
+//         to click "Save" button as former cy.clck("Save") became ambiguous.
+// Git sha: f90e4db22e0b668600846e00ed4d1e28d8f92d95
+// Date: 2024-02-23
+// Author: markbastian
+const updatedCypressSaveLogicVersion = 49;
+
+// Change: Sample data times changed.
+// TODO: The specific version in which this changed hasn't been tracked down
+// yet, but this is an upper bound. If more cross-version checks are added for
+// earlier versions and this breaks, this number may need to be adjusted down.
+export const sampleDataTimesChangedVersion = 47;
+
+// Change: In older versions of metabase, when a question was loaded a modal was
+// presented stating "It's okay to play around with saved questions". The user
+// was required to press "Okay" to dismiss this modal. In later versions this
+// modal was not presented in the same location.
+// TODO: The specific version in which this changed hasn't been tracked down
+// yet, but this is an upper bound. If more cross-version checks are added for
+// earlier versions and this breaks, this number may need to be adjusted down.
+export const questionsAreOkToPlayWithModalVersion = 45;
+
+// Change: Older versions of Metabase presented a 3-panel display, the center
+// panel being "Custom question". In later generations, this was accessible via
+// clicking "New" -> "Question" at the top right corner in the UI.
+// TODO: The specific version in which this changed hasn't been tracked down
+// yet, but this is an upper bound. If more cross-version checks are added for
+// earlier versions and this breaks, this number may need to be adjusted down.
+export const newQuestionMenuVersion = 45;
+
+// Change: In older versions of metabase, area and line charts were selectable
+// Visualization types, but the actual operation to fill the area was to select
+// the "area" icon in the Display settings, which is really weird.
+// TODO: The specific version in which this changed hasn't been tracked down
+// yet, but this is an upper bound. If more cross-version checks are added for
+// earlier versions and this breaks, this number may need to be adjusted down.
+export const filledAreaIconRemovedVersion = 45;
+
+export function setupLanguage() {
+  // Make sure English is the default selected language
+  cy.findByText("English")
+    .should("have.css", "background-color")
+    .and("eq", "rgb(80, 158, 227)");
+
+  cy.button("Next").click();
+  cy.findByText("Your language is set to English");
+}
+
+export function setupInstance({ version, majorVersion }) {
+  const companyLabel =
+    version === "v0.41.3.1"
+      ? "Your company or team name"
+      : "Company or team name";
+
+  const finalSetupButton = version === "v0.41.3.1" ? "Next" : "Finish";
+
+  cy.findByLabelText("First name").type("Superuser");
+  cy.findByLabelText("Last name").type("Tableton");
+  cy.findByLabelText("Email").type("admin@metabase.test");
+  cy.findByLabelText(companyLabel).type("Metabase");
+  cy.findByLabelText("Create a password").type("12341234");
+  cy.findByLabelText("Confirm your password").type("12341234");
+  cy.button("Next").click();
+  cy.findByText("Hi, Superuser. Nice to meet you!");
+
+  // A "What will you use Metabase for?" prompt exists in later versions.
+  // If it exists, click through.
+  if (majorVersion >= metabasePurposeVersion) {
+    cy.button("Next").click();
+  }
+
+  cy.findByText("I'll add my data later").click();
+  cy.findByText("I'll add my own data later");
+
+  // Collection defaults to on and describes data collection
+  cy.findByText("All collection is completely anonymous.");
+  // turn collection off, which hides data collection description
+  cy.findByLabelText(
+    "Allow Metabase to anonymously collect usage events",
+  ).click();
+  cy.findByText("All collection is completely anonymous.").should("not.exist");
+  cy.findByText(finalSetupButton).click();
+  cy.findByText("Take me to Metabase").click();
+
+  cy.location("pathname").should("eq", "/");
+  cy.contains("Reviews");
+}
+
+export function newQuestion({ majorVersion }) {
+  if (majorVersion < newQuestionMenuVersion) {
+    cy.findByText("Custom question").click();
+  } else {
+    cy.button("New").click();
+    cy.findByText("Question").click();
+  }
+}
+
+export function saveQuestion({ majorVersion }) {
+  if (majorVersion < updatedCypressSaveLogicVersion) {
+    cy.button("Save").click();
+  } else {
+    cy.findByTestId("save-question-modal").within(modal => {
+      cy.findByText("Save").click();
+    });
+  }
+}
+
+export function assertTimelineData({ majorVersion }) {
+  if (majorVersion < sampleDataTimesChangedVersion) {
+    cy.get(".x.axis .tick")
+      .should("contain", "Q1 - 2017")
+      .and("contain", "Q1 - 2018")
+      .and("contain", "Q1 - 2019")
+      .and("contain", "Q1 - 2020");
+  } else {
+    cy.get(".x.axis .tick")
+      .should("contain", "Q1 2023")
+      .and("contain", "Q1 2024")
+      .and("contain", "Q1 2025")
+      .and("contain", "Q1 2026");
+  }
+}
+
+export function dismissOkToPlayWithQuestionsModal({ majorVersion }) {
+  if (majorVersion < questionsAreOkToPlayWithModalVersion) {
+    cy.findByText("It's okay to play around with saved questions");
+    cy.button("Okay").click();
+  }
+}
+
+export function fillAreaUnderLineChart({ majorVersion }) {
+  if (majorVersion < filledAreaIconRemovedVersion) {
+    cy.findByTestId("sidebar-left").within(element => {
+      cy.icon("area").click();
+    });
+  }
+}
diff --git a/e2e/test/scenarios/cross-version/source/00-setup.cy.spec.js b/e2e/test/scenarios/cross-version/source/00-setup.cy.spec.js
index 50838f0d8037a16f201bc2ee4557104703109d5c..6c1cb826f6818956f76f774189317377a07019f4 100644
--- a/e2e/test/scenarios/cross-version/source/00-setup.cy.spec.js
+++ b/e2e/test/scenarios/cross-version/source/00-setup.cy.spec.js
@@ -1,8 +1,9 @@
 import {
-  version,
   setupLanguage,
   setupInstance,
-} from "./helpers/cross-version-source-helpers";
+} from "e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js";
+
+import { version } from "./helpers/cross-version-source-helpers";
 
 describe(`setup on ${version}`, () => {
   it("should set up metabase", () => {
@@ -14,10 +15,12 @@ describe(`setup on ${version}`, () => {
     // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
     cy.findByText("Let's get started").click();
 
-    setupLanguage();
+    setupLanguage(version);
     setupInstance(version);
 
     cy.visit("/admin");
-    cy.icon("store");
+    // Find an element on the admin page so we know we've landed.
+    // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
+    cy.findByText("Setup");
   });
 });
diff --git a/e2e/test/scenarios/cross-version/source/02-datamodel.cy.spec.js b/e2e/test/scenarios/cross-version/source/02-datamodel.cy.spec.js
index 87e06e0593a2a193c42988c2e7bfa51e9a3370ac..d25d5c94f61da1cb26d881c6a2dbcf891efbee61 100644
--- a/e2e/test/scenarios/cross-version/source/02-datamodel.cy.spec.js
+++ b/e2e/test/scenarios/cross-version/source/02-datamodel.cy.spec.js
@@ -21,7 +21,13 @@ it("should configure data model settings", () => {
   );
 
   cy.get(".AdminList").findByText("Orders").click();
-  cy.findByDisplayValue("Product ID").parent().find(".Icon-gear").click();
+
+  cy.findByDisplayValue("Product ID")
+    .parent()
+    .parent()
+    .find(".Icon-gear")
+    .click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Use original value").click();
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -36,7 +42,8 @@ it("should configure data model settings", () => {
     "remapRatingValues",
   );
 
-  cy.findByDisplayValue("Rating").parent().find(".Icon-gear").click();
+  cy.findByDisplayValue("Rating").parent().parent().find(".Icon-gear").click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Use original value").click();
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -67,13 +74,21 @@ it("should configure data model settings", () => {
   cy.get(".AdminList").findByText("Products").click();
 
   cy.intercept("PUT", `/api/field/${PRODUCTS.EAN}`).as("hideEan");
-  cy.findByDisplayValue("Ean").parent().contains("Everywhere").click();
+
+  cy.findByDisplayValue("Ean").parent().parent().contains("Everywhere").click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Do not include").click();
   cy.wait("@hideEan");
 
   cy.intercept("PUT", `/api/field/${PRODUCTS.PRICE}`).as("updatePriceField");
-  cy.findByDisplayValue("Price").parent().contains("No semantic type").click();
+
+  cy.findByDisplayValue("Price")
+    .parent()
+    .parent()
+    .contains("No semantic type")
+    .click();
+
   cy.get(".MB-Select")
     .scrollTo("top")
     .within(() => {
@@ -91,7 +106,13 @@ it("should configure data model settings", () => {
   cy.get(".AdminList").findByText("People").click();
 
   cy.intercept("PUT", `/api/field/${PEOPLE.PASSWORD}`).as("hidePassword");
-  cy.findByDisplayValue("Password").parent().contains("Everywhere").click();
+
+  cy.findByDisplayValue("Password")
+    .parent()
+    .parent()
+    .contains("Everywhere")
+    .click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Do not include").click();
   cy.wait("@hidePassword");
diff --git a/e2e/test/scenarios/cross-version/source/03-questions.cy.spec.js b/e2e/test/scenarios/cross-version/source/03-questions.cy.spec.js
index 7a6a1c5a3c03c3ec6c88bb9dcab8e6155cb98fc7..82d0c42ca9bfd9d366d4a83c5e998f02d978bda4 100644
--- a/e2e/test/scenarios/cross-version/source/03-questions.cy.spec.js
+++ b/e2e/test/scenarios/cross-version/source/03-questions.cy.spec.js
@@ -1,13 +1,22 @@
 import { visualize } from "e2e/support/helpers";
+import {
+  fillAreaUnderLineChart,
+  newQuestion,
+  saveQuestion,
+} from "e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js";
+
+import { version } from "./helpers/cross-version-source-helpers";
 
 it("should create questions", () => {
   cy.signInAsAdmin();
 
   cy.visit("/question/new");
-  // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
-  cy.findByText("Custom question").click();
+
+  newQuestion(version);
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Orders").click();
+
   cy.icon("join_left_outer").click();
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Products").click();
@@ -31,7 +40,12 @@ it("should create questions", () => {
 
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Pick a column to group by").click();
-  cy.get(".List-section-title").contains("Products").click();
+
+  // Older versions were Products, newer use Product
+  cy.get(".List-section-title")
+    .contains(/Products?/)
+    .click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Category").click();
 
@@ -40,6 +54,13 @@ it("should create questions", () => {
   cy.get(".bar").should("have.length", 4);
 
   cy.findByTestId("viz-settings-button").click();
+
+  //NOTE: In older versions of Metabase, Display is selected by default. Newer
+  // versions default to Data. This will ensure we've selected the right tab
+  // either way.
+  // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
+  cy.findByText("Display").click();
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.contains("Show values on data points").next().click();
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -52,13 +73,16 @@ it("should create questions", () => {
     "The average rating of our top selling products broken down into categories.",
     { delay: 0 },
   );
-  cy.button("Save").click();
+
+  saveQuestion(version);
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Not now").click();
 
   cy.visit("/question/new");
-  // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
-  cy.findByText("Custom question").click();
+
+  newQuestion(version);
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText(/Sample (Dataset|Database)/).click();
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -86,8 +110,14 @@ it("should create questions", () => {
   visualize();
   cy.get("circle");
 
-  cy.findByTestId("viz-settings-button").click();
-  cy.icon("area").click();
+  cy.findByTestId("viz-type-button").click();
+  cy.findByTestId("Area-button").click();
+
+  // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
+  cy.findByText("Display").click();
+
+  fillAreaUnderLineChart(version);
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Goal line").next().click();
   cy.findByDisplayValue("0").type("100000").blur();
@@ -98,7 +128,9 @@ it("should create questions", () => {
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Save").click();
   cy.findByLabelText("Name").clear().type("Quarterly Revenue");
-  cy.button("Save").click();
+
+  saveQuestion(version);
+
   // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
   cy.findByText("Not now").click();
 });
diff --git a/e2e/test/scenarios/cross-version/source/helpers/cross-version-source-helpers.js b/e2e/test/scenarios/cross-version/source/helpers/cross-version-source-helpers.js
index cdfc9f4eb35bb06f74c338b3a13a56e48aad76b5..fd0d967def067e2feaee63a72ac1ea48ea927aff 100644
--- a/e2e/test/scenarios/cross-version/source/helpers/cross-version-source-helpers.js
+++ b/e2e/test/scenarios/cross-version/source/helpers/cross-version-source-helpers.js
@@ -1,45 +1,3 @@
-export const version = Cypress.env("SOURCE_VERSION");
+import { parseVersionString } from "e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js";
 
-export function setupLanguage() {
-  // Make sure English is the default selected language
-  cy.findByText("English")
-    .should("have.css", "background-color")
-    .and("eq", "rgb(80, 158, 227)");
-
-  cy.button("Next").click();
-  cy.findByText("Your language is set to English");
-}
-
-export function setupInstance(version) {
-  const companyLabel =
-    version === "v0.41.3.1"
-      ? "Your company or team name"
-      : "Company or team name";
-
-  const finalSetupButton = version === "v0.41.3.1" ? "Next" : "Finish";
-
-  cy.findByLabelText("First name").type("Superuser");
-  cy.findByLabelText("Last name").type("Tableton");
-  cy.findByLabelText("Email").type("admin@metabase.test");
-  cy.findByLabelText(companyLabel).type("Metabase");
-  cy.findByLabelText("Create a password").type("12341234");
-  cy.findByLabelText("Confirm your password").type("12341234");
-  cy.button("Next").click();
-  cy.findByText("Hi, Superuser. Nice to meet you!");
-
-  cy.findByText("I'll add my data later").click();
-  cy.findByText("I'll add my own data later");
-
-  // Collection defaults to on and describes data collection
-  cy.findByText("All collection is completely anonymous.");
-  // turn collection off, which hides data collection description
-  cy.findByLabelText(
-    "Allow Metabase to anonymously collect usage events",
-  ).click();
-  cy.findByText("All collection is completely anonymous.").should("not.exist");
-  cy.findByText(finalSetupButton).click();
-  cy.findByText("Take me to Metabase").click();
-
-  cy.location("pathname").should("eq", "/");
-  cy.contains("Reviews");
-}
+export const version = parseVersionString(Cypress.env("SOURCE_VERSION"));
diff --git a/e2e/test/scenarios/cross-version/target/helpers/cross-version-target-helpers.js b/e2e/test/scenarios/cross-version/target/helpers/cross-version-target-helpers.js
index a6c1895a31b37de52247b8ff1b75d434dc66927e..500089740917b4f1fae3cf794ea592e07ca91b7a 100644
--- a/e2e/test/scenarios/cross-version/target/helpers/cross-version-target-helpers.js
+++ b/e2e/test/scenarios/cross-version/target/helpers/cross-version-target-helpers.js
@@ -1 +1,3 @@
-export const version = Cypress.env("TARGET_VERSION");
+import { parseVersionString } from "e2e/test/scenarios/cross-version/helpers/cross-version-helpers.js";
+
+export const version = parseVersionString(Cypress.env("TARGET_VERSION"));
diff --git a/e2e/test/scenarios/cross-version/target/smoke.cy.spec.js b/e2e/test/scenarios/cross-version/target/smoke.cy.spec.js
index 3ba74bd5d3a03a9db2b66a8dcea4cf236bb1c523..04a7efd5dfdc5f9f16f015fe0190a3aaf4764bb5 100644
--- a/e2e/test/scenarios/cross-version/target/smoke.cy.spec.js
+++ b/e2e/test/scenarios/cross-version/target/smoke.cy.spec.js
@@ -1,4 +1,8 @@
-import { version } from "./helpers/cross-version-target-helpers";
+import {
+  assertTimelineData,
+  dismissOkToPlayWithQuestionsModal,
+} from "e2e/test/scenarios/cross-version/helpers/cross-version-helpers";
+import { version } from "e2e/test/scenarios/cross-version/source/helpers/cross-version-source-helpers.js";
 
 describe(`smoke test the migration to the version ${version}`, () => {
   it("should already be set up", () => {
@@ -20,9 +24,7 @@ describe(`smoke test the migration to the version ${version}`, () => {
     cy.findByText("Quarterly Revenue").click();
     cy.wait("@cardQuery");
 
-    // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
-    cy.findByText("It's okay to play around with saved questions");
-    cy.button("Okay").click();
+    dismissOkToPlayWithQuestionsModal(version);
 
     cy.get("circle");
     cy.get(".line");
@@ -30,11 +32,8 @@ describe(`smoke test the migration to the version ${version}`, () => {
     cy.findByText("Goal");
     cy.get(".x-axis-label").invoke("text").should("eq", "Created At");
     cy.get(".y-axis-label").invoke("text").should("eq", "Revenue");
-    cy.get(".x.axis .tick")
-      .should("contain", "Q1 2023")
-      .and("contain", "Q1 2024")
-      .and("contain", "Q1 2025")
-      .and("contain", "Q1 2026");
+
+    assertTimelineData(version);
 
     cy.get(".y.axis .tick")
       .should("contain", "20,000")