diff --git a/ee/spec/features/admin/admin_runners_spec.rb b/ee/spec/features/admin/admin_runners_spec.rb
index ba96f32d9405fb28d96c8b68ba8b02950f2dcd03..0331fc4f04a6d8c4e8ca8928182e4e992dbf37f5 100644
--- a/ee/spec/features/admin/admin_runners_spec.rb
+++ b/ee/spec/features/admin/admin_runners_spec.rb
@@ -3,6 +3,7 @@
 require 'spec_helper'
 
 RSpec.describe "Admin Runners" do
+  include StubVersion
   include Spec::Support::Helpers::Features::RunnersHelpers
 
   let_it_be(:admin) { create(:admin) }
@@ -19,7 +20,7 @@
       let(:runner) { create(:ci_runner, :instance, version: runner_version) }
 
       before do
-        stub_const('::Gitlab::VERSION', '15.1.0')
+        stub_version('15.1.0', 'unused_revision')
 
         url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
         WebMock.stub_request(:get, url).to_return(
diff --git a/ee/spec/frontend/fixtures/runner.rb b/ee/spec/frontend/fixtures/runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c4f704c7e1edae38bc41b943874796ec1a23bef
--- /dev/null
+++ b/ee/spec/frontend/fixtures/runner.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Runner EE (JavaScript fixtures)' do
+  include StubVersion
+  include AdminModeHelper
+  include ApiHelpers
+  include JavaScriptFixturesHelpers
+  include GraphqlHelpers
+
+  let_it_be(:admin) { create(:admin) }
+
+  query_path = 'runner/graphql/'
+  fixtures_path = 'graphql/runner/'
+
+  describe 'as admin', GraphQL::Query do
+    before do
+      sign_in(admin)
+      enable_admin_mode!(admin)
+    end
+
+    describe 'all_runners.query.graphql', type: :request do
+      let_it_be(:upgrade_available_runner) { create(:ci_runner, :instance, version: '15.0.0') }
+      let_it_be(:upgrade_recommended_runner) { create(:ci_runner, :instance, version: '15.1.0') }
+      let_it_be(:up_to_date_runner) { create(:ci_runner, :instance, version: '15.1.1') }
+
+      all_runners_query = 'list/all_runners.query.graphql'
+
+      let_it_be(:query) do
+        get_graphql_query_as_string("#{query_path}#{all_runners_query}")
+      end
+
+      before do
+        stub_licensed_features(runner_upgrade_management: true)
+
+        stub_version('15.1.0', 'unused_revision')
+        available_runner_releases = %w[15.0.0 15.1.0 15.1.1]
+
+        url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+        WebMock.stub_request(:get, url).to_return(
+          body: available_runner_releases.map { |v| { name: v } }.to_json,
+          status: 200,
+          headers: { 'Content-Type' => 'application/json' }
+        )
+      end
+
+      it "#{fixtures_path}#{all_runners_query}.upgrade_status.json" do
+        post_graphql(query, current_user: admin, variables: {})
+
+        expect_graphql_errors_to_be_empty
+      end
+    end
+  end
+end
diff --git a/ee/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/ee/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..360228f30e78fdd6e9f981d0f3ecc0291e618a3d
--- /dev/null
+++ b/ee/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { s__ } from '~/locale';
+
+import { createLocalState } from '~/runner/graphql/list/local_state';
+import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
+import RunnerList from '~/runner/components/runner_list.vue';
+
+import RunnerUpgradeStatusBadge from 'ee_component/runner/components/runner_upgrade_status_badge.vue';
+
+import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
+import allRunnersCountQuery from '~/runner/graphql/list/all_runners_count.query.graphql';
+
+import {
+  runnersCountData,
+  onlineContactTimeoutSecs,
+  staleTimeoutSecs,
+  emptyStateSvgPath,
+  emptyStateFilteredSvgPath,
+} from 'jest/runner/mock_data';
+import { allRunnersUpgradeStatusData } from '../mock_data';
+
+const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+
+const mockRunnersHandler = jest.fn();
+const mockRunnersCountHandler = jest.fn();
+
+Vue.use(VueApollo);
+
+describe('AdminRunnersApp', () => {
+  let wrapper;
+  let cacheConfig;
+  let localMutations;
+
+  const findRunnerRows = () => wrapper.findComponent(RunnerList).findAll('tr');
+
+  const createComponent = ({ props = {}, provide, ...options } = {}) => {
+    ({ cacheConfig, localMutations } = createLocalState());
+
+    const handlers = [
+      [allRunnersQuery, mockRunnersHandler],
+      [allRunnersCountQuery, mockRunnersCountHandler],
+    ];
+
+    wrapper = mountExtended(AdminRunnersApp, {
+      apolloProvider: createMockApollo(handlers, {}, cacheConfig),
+      propsData: {
+        registrationToken: mockRegistrationToken,
+        ...props,
+      },
+      provide: {
+        localMutations,
+        onlineContactTimeoutSecs,
+        staleTimeoutSecs,
+        emptyStateSvgPath,
+        emptyStateFilteredSvgPath,
+        ...provide,
+      },
+      ...options,
+    });
+
+    return waitForPromises();
+  };
+
+  beforeEach(() => {
+    mockRunnersHandler.mockResolvedValue(allRunnersUpgradeStatusData);
+    mockRunnersCountHandler.mockResolvedValue(runnersCountData);
+  });
+
+  afterEach(() => {
+    mockRunnersHandler.mockReset();
+    mockRunnersCountHandler.mockReset();
+    wrapper.destroy();
+  });
+
+  describe('upgrade badges', () => {
+    let rows = null;
+
+    beforeEach(async () => {
+      await createComponent({
+        provide: { glFeatures: { runnerUpgradeManagement: true } },
+      });
+    });
+
+    it.each`
+      version     | description                                 | upgradeText                           | index
+      ${'15.1.1'} | ${'displays no upgrade badge (up to date)'} | ${''}                                 | ${1}
+      ${'15.1.0'} | ${'displays upgrade recommended'}           | ${s__('Runners|upgrade recommended')} | ${2}
+      ${'15.0.0'} | ${'displays upgrade available'}             | ${s__('Runners|upgrade available')}   | ${3}
+    `('with $version $description', ({ version, index, upgradeText }) => {
+      rows = findRunnerRows().wrappers.map(extendedWrapper);
+
+      expect(rows[index].findByText(version).exists()).toBe(true);
+      expect(rows[index].find(RunnerUpgradeStatusBadge).text()).toBe(upgradeText);
+    });
+  });
+});
diff --git a/ee/spec/frontend/runner/mock_data.js b/ee/spec/frontend/runner/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..825ee099651b1dabcca9d16f2f0db7c3e7e73879
--- /dev/null
+++ b/ee/spec/frontend/runner/mock_data.js
@@ -0,0 +1,6 @@
+// Fixtures generated by: spec/frontend/fixtures/runner.rb
+
+// List queries
+import allRunnersUpgradeStatusData from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.upgrade_status.json';
+
+export { allRunnersUpgradeStatusData };