diff --git a/.gitignore b/.gitignore
index 2ec1b6e1a..e2daf2298 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,3 +132,6 @@ client/resources
#temp files for import-tests
server/scripts/import-tests/tmp
+
+#IDE settings
+.vscode/settings.json
\ No newline at end of file
diff --git a/client/components/ReportRerun/ReportRerun.module.css b/client/components/ReportRerun/ReportRerun.module.css
new file mode 100644
index 000000000..80994fc5f
--- /dev/null
+++ b/client/components/ReportRerun/ReportRerun.module.css
@@ -0,0 +1,445 @@
+.test-plan-refresh {
+ max-width: 1200px;
+}
+
+.events-header,
+.rerun-header {
+ font-size: var(--28px);
+ margin-bottom: var(--32px);
+ color: var(--font-black);
+ font-weight: var(--font-weight-medium);
+}
+
+.rerun-dashboard {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--section-gap);
+ margin-bottom: var(--section-gap);
+ width: 100%;
+ max-width: 100%;
+ overflow-x: hidden;
+}
+
+@media (min-width: 768px) {
+ .rerun-dashboard {
+ grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
+ }
+}
+
+.rerun-opportunity {
+ height: min-content;
+ background-color: white;
+ border: 1px solid var(--border-gray);
+ border-radius: 0.5rem;
+ padding: var(--section-gap);
+ margin-bottom: 1.5rem;
+ box-shadow: 0 1px 3px var(--box-shadow-gray);
+ transition: box-shadow 0.2s ease-in-out;
+ width: 100%;
+ max-width: 100%;
+ overflow-x: hidden;
+}
+
+@media (min-width: 768px) {
+ .rerun-opportunity {
+ padding: 1.5rem;
+ }
+}
+
+.opportunity-header {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid var(--border-gray);
+ padding: var(--section-gap);
+ display: flex;
+ align-items: center;
+}
+
+.bot-name {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--darkest-gray);
+ margin: 0 0 var(--section-gap) 0;
+}
+
+.version-badge {
+ display: inline-block;
+ background-color: #e9ecef;
+ color: var(--dark-gray);
+ font-size: var(--nav-font-size-sm);
+ padding: var(--text-gap) var(--el-gap);
+ border-radius: 4px;
+ margin-top: var(--el-gap);
+}
+
+.version-update {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin: var(--section-gap) 0;
+ padding: var(--section-gap);
+ background-color: #f8f9fa;
+ border-radius: 0.75rem;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+@media (min-width: 768px) {
+ .version-update {
+ margin: 1.25rem 0;
+ padding: 1.5rem;
+ overflow-x: visible;
+ }
+}
+
+.version-info {
+ display: flex;
+ align-items: center;
+ width: fit-content;
+ min-width: 100%;
+ position: relative;
+ min-height: 90px;
+ padding: 0 55px 0 0;
+}
+
+@media (min-width: 768px) {
+ .version-info {
+ min-height: 110px;
+ }
+}
+
+.version-groups-container {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ position: relative;
+ z-index: 1;
+}
+
+/* Simple line connecting versions */
+.version-info::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background-color: #64748b;
+ opacity: 0.3;
+ z-index: 0;
+}
+
+/* Hide the line when there are no previous versions */
+.version-info:not(:has(.version-groups-container > *))::after {
+ display: none;
+}
+
+/* Center the highlight box when there are no previous versions */
+.version-info:not(:has(.version-groups-container > *)) .version-box.highlight {
+ position: relative;
+ left: 50%;
+ transform: translate(-25%, 0);
+ width: 120px;
+}
+
+.version-box {
+ background: white;
+ width: 90px;
+ height: 90px;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-right: -2rem;
+ position: relative;
+ box-shadow: 0 2px 4px var(--box-shadow-gray);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ padding: 0.75rem;
+ text-align: center;
+ z-index: 1;
+ cursor: default;
+}
+
+@media (min-width: 768px) {
+ .version-box {
+ width: 80px;
+ height: 80px;
+ min-width: 80px;
+ margin-right: -1rem;
+ padding: var(--section-gap);
+ }
+}
+
+.version-box:hover {
+ transform: translateY(-2px);
+ z-index: 2;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
+}
+
+.version-box .version-number {
+ font-weight: 600;
+ font-size: var(--default-font-size);
+ margin: 0;
+ color: var(--rd-color);
+ line-height: 1.2;
+}
+
+@media (min-width: 768px) {
+ .version-box .version-number {
+ font-size: var(--default-font-size);
+ }
+}
+
+.version-box .version-count {
+ font-size: 0.75rem;
+ color: var(--dark-gray-text);
+ margin: 0;
+ line-height: 1.2;
+}
+
+@media (min-width: 768px) {
+ .version-box .version-count {
+ font-size: var(--nav-font-size-sm);
+ }
+}
+
+.version-box.highlight {
+ background-color: #f0f9ff;
+ border-color: var(--border-dark-blue);
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ box-shadow: 0 2px 4px var(--button-shadow);
+ margin-right: 0;
+ z-index: 1;
+ width: 90px;
+ height: 90px;
+}
+
+.version-box.highlight .version-number {
+ font-size: var(--default-font-size);
+ color: var(--font-link);
+}
+
+@media (min-width: 768px) {
+ .version-box.highlight .version-number {
+ font-size: var(--default-font-size);
+ }
+}
+
+.version-box.highlight:hover {
+ transform: translateY(-50%);
+ box-shadow: 0 4px 6px var(--button-shadow);
+}
+
+.version-box.highlight.no-reports {
+ background-color: #f8fafc;
+ border-color: #e2e8f0;
+ box-shadow: 0 2px 4px var(--box-shadow-gray);
+}
+
+.version-box.highlight.no-reports .version-number {
+ color: #64748b;
+}
+
+.version-box.highlight.no-reports:hover {
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
+}
+
+.version-separator {
+ display: none;
+}
+
+.test-plans-preview {
+ margin-top: 2rem;
+}
+
+.version-group {
+ margin-bottom: 1.5rem;
+}
+
+.version-group:last-child {
+ margin-bottom: 0;
+}
+
+.plans-preview-title {
+ font-size: var(--default-font-size);
+ color: var(--dark-gray-text);
+ margin-bottom: var(--el-gap);
+}
+
+.plan-summary {
+ margin: 1.25rem 0;
+ padding: var(--el-gap);
+ background-color: #f6f8fa;
+ border-radius: 0.25rem;
+}
+
+.plan-count {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: default;
+ user-select: none;
+ justify-content: center;
+}
+
+.plan-count-number {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--font-link);
+}
+
+@media (min-width: 768px) {
+ .plan-count-number {
+ font-size: 1.5rem;
+ }
+}
+
+.plan-count-label {
+ font-size: var(--nav-font-size-sm);
+ color: var(--dark-gray-text);
+}
+
+@media (min-width: 768px) {
+ .plan-count-label {
+ font-size: var(--default-font-size);
+ }
+}
+
+.plans-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ overflow-y: auto;
+ border: 1px solid #e1e4e8;
+ border-radius: 0.5rem;
+}
+
+.plans-list li {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid #e1e4e8;
+ color: var(--dark-gray-text);
+ cursor: default;
+}
+
+.plans-list li:last-child {
+ border-bottom: none;
+}
+
+.action-footer {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 1.5rem;
+}
+
+.rerun-button {
+ background-color: var(--positive-green);
+ color: white;
+ border: none;
+ padding: 0.625rem 1.25rem;
+ border-radius: 4px;
+ font-size: var(--default-font-size);
+ font-weight: var(--font-weight-medium);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 140px;
+}
+
+.rerun-button:disabled {
+ background-color: var(--border-gray);
+ cursor: not-allowed;
+}
+
+.rerun-button:not(:disabled):hover {
+ background-color: var(--rd-color);
+ cursor: pointer;
+}
+
+.rerun-button:focus {
+ box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.3);
+ outline: none;
+}
+
+.events-section {
+ background-color: white;
+ border: 1px solid #e1e4e8;
+ border-radius: 0.5rem;
+ padding: 0;
+ margin-top: 2.5rem;
+ box-shadow: 0 1px 3px var(--box-shadow-gray);
+ transition: box-shadow 0.2s ease-in-out;
+ overflow: hidden;
+}
+
+.rerun-header,
+.events-header {
+ font-size: 1.25rem;
+ margin: 0;
+ padding: 1.25rem;
+ border-bottom: 1px solid #dee2e6;
+ color: #24292f;
+ font-weight: 600;
+ background-color: #f8f9fa;
+}
+
+.events-content {
+ padding: 0;
+}
+
+.empty-events-message {
+ padding: 1.5rem;
+ color: var(--dark-gray-text);
+ font-size: var(--default-font-size);
+ text-align: center;
+ margin: 0;
+}
+
+.events-section .theme-table {
+ margin: 0;
+ border: none;
+}
+
+.events-section th {
+ background-color: #f8f9fa;
+ font-weight: 600;
+ padding: var(--section-gap) 1.25rem;
+ color: #24292f;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.events-section td {
+ padding: var(--section-gap) 1.25rem;
+ color: var(--dark-gray-text);
+ line-height: 1.5;
+ border-top: 1px solid var(--border-gray);
+}
+
+.timestamp-cell {
+ white-space: nowrap;
+ width: 180px;
+ color: var(--dark-gray-text);
+ font-weight: var(--font-weight-medium);
+}
+
+.message-cell {
+ line-height: 1.6;
+ color: var(--font-black);
+}
+
+.events-header-container {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.events-header-container h2 {
+ margin: 0;
+ font-size: inherit;
+ font-weight: inherit;
+ color: inherit;
+}
diff --git a/client/components/ReportRerun/RerunDashboard.jsx b/client/components/ReportRerun/RerunDashboard.jsx
new file mode 100644
index 000000000..0bf48cd49
--- /dev/null
+++ b/client/components/ReportRerun/RerunDashboard.jsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './ReportRerun.module.css';
+
+const RerunDashboard = ({ activeRuns, onRerunClick }) => (
+ <>
+
+ {activeRuns.map(run => {
+ const totalTestPlans = run.reportGroups.reduce(
+ (sum, group) => sum + group.reportCount,
+ 0
+ );
+ const versionDescription = `${run.botName} ${
+ run.newVersion
+ } automation support has been added to the application. ${totalTestPlans} test plan versions can be re-run from ${
+ run.reportGroups.length
+ } previous versions, including ${run.reportGroups
+ .map(g => g.prevVersion)
+ .join(', ')}.`;
+
+ return (
+
+
+ {run.botName} {run.newVersion}
+
+
+
{versionDescription}
+
+
+
+
+ {run.reportGroups.map((group, index) => (
+
+
+ {group.prevVersion}
+
+
+ {group.reportCount} run{group.reportCount !== 1 && 's'}
+
+
+ ))}
+
+
+ {run.newVersion}
+ {!run.reportGroups.length && (
+
+ No reports to update
+
+ )}
+
+
+
+
+
+ {run.reportGroups.length ? (
+
+
+ {totalTestPlans}
+
+
+ {totalTestPlans === 1
+ ? 'Test plan version can be re-run'
+ : 'Test plan versions can be re-run'}
+
+
+ ) : (
+
+
+ No reports available for update
+
+
+ )}
+
+
+
+ {run.reportGroups.map((group, index) => (
+
+
+ {group.reportCount} from {group.prevVersion}
+
+
+ {group.reports.map((report, idx) => (
+
+ {report.testPlanVersion.title}{' '}
+ {report.testPlanVersion.versionString},{' '}
+ {report.browser.name}
+
+ ))}
+
+
+ ))}
+
+
+
+ onRerunClick(run)}
+ aria-label={`Start automated test plan runs for ${totalTestPlans} test plan versions using ${run.botName} ${run.newVersion}`}
+ >
+ Start Updates
+
+
+
+ );
+ })}
+
+ >
+);
+
+RerunDashboard.propTypes = {
+ activeRuns: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ botName: PropTypes.string.isRequired,
+ newVersion: PropTypes.string.isRequired,
+ reportGroups: PropTypes.arrayOf(
+ PropTypes.shape({
+ prevVersion: PropTypes.string.isRequired,
+ reportCount: PropTypes.number.isRequired,
+ reports: PropTypes.arrayOf(
+ PropTypes.shape({
+ testPlanVersion: PropTypes.shape({
+ title: PropTypes.string.isRequired,
+ versionString: PropTypes.string.isRequired
+ }).isRequired,
+ browser: PropTypes.shape({
+ name: PropTypes.string.isRequired
+ }).isRequired
+ })
+ ).isRequired
+ })
+ ).isRequired
+ })
+ ).isRequired,
+ onRerunClick: PropTypes.func.isRequired
+};
+
+export default RerunDashboard;
diff --git a/client/components/ReportRerun/UpdateEventsPanel.jsx b/client/components/ReportRerun/UpdateEventsPanel.jsx
new file mode 100644
index 000000000..bccacdb12
--- /dev/null
+++ b/client/components/ReportRerun/UpdateEventsPanel.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import RefreshButton from '../common/RefreshButton';
+import { convertStringFormatToAnotherFormat } from 'shared/dates';
+import { Table } from 'react-bootstrap';
+import styles from './ReportRerun.module.css';
+
+const UpdateEventsPanel = ({ events = [], isAdmin, onRefresh }) => {
+ return (
+
-
- Test Queue | ARIA-AT
-
- Test Queue
+ const renderQueueContent = () => (
+ <>
{hasTestPlanReports && (
{isAdmin
@@ -316,7 +314,6 @@ const TestQueue = () => {
{testPlans.length
? testPlans.map(testPlan => (
- {/* ID needed for recovering focus after deleting a report */}
{testPlan.title}
@@ -324,6 +321,27 @@ const TestQueue = () => {
))
: null}
+ >
+ );
+
+ const tabs = [
+ {
+ label: 'Manual Test Queue',
+ content: renderQueueContent()
+ },
+ {
+ label: 'Automated Report Updates',
+ content:
+ }
+ ];
+
+ return (
+
+
+ Test Queue | ARIA-AT
+
+ Test Queue
+
);
};
diff --git a/client/components/common/RefreshButton/RefreshButton.module.css b/client/components/common/RefreshButton/RefreshButton.module.css
new file mode 100644
index 000000000..e17d057df
--- /dev/null
+++ b/client/components/common/RefreshButton/RefreshButton.module.css
@@ -0,0 +1,21 @@
+.refresh-button {
+ background: none;
+ border: none;
+ padding: 0.25rem;
+ cursor: pointer;
+ color: inherit;
+ font-size: 0.875rem;
+}
+
+.refresh-button:hover {
+ opacity: 0.8;
+}
+
+.refresh-button:disabled {
+ cursor: default;
+ opacity: 0.7;
+}
+
+.refresh-button .fa-spin {
+ animation-duration: 1.5s;
+}
diff --git a/client/components/common/RefreshButton/index.jsx b/client/components/common/RefreshButton/index.jsx
new file mode 100644
index 000000000..76383aaf0
--- /dev/null
+++ b/client/components/common/RefreshButton/index.jsx
@@ -0,0 +1,45 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import styles from './RefreshButton.module.css';
+
+const RefreshButton = ({ onRefresh }) => {
+ const [isRefreshing, setIsRefreshing] = useState(false);
+
+ const handleRefresh = async () => {
+ setIsRefreshing(true);
+ const startTime = Date.now();
+ try {
+ await onRefresh();
+ const elapsedTime = Date.now() - startTime;
+ const minimumDuration = 750;
+ // Allows for a full revolution of the spinner
+ if (elapsedTime < minimumDuration) {
+ await new Promise(resolve =>
+ setTimeout(resolve, minimumDuration - elapsedTime)
+ );
+ }
+ } finally {
+ setIsRefreshing(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+RefreshButton.propTypes = {
+ onRefresh: PropTypes.func.isRequired
+};
+
+export default RefreshButton;
diff --git a/client/components/common/Tabs/Tabs.jsx b/client/components/common/Tabs/Tabs.jsx
new file mode 100644
index 000000000..4612083c9
--- /dev/null
+++ b/client/components/common/Tabs/Tabs.jsx
@@ -0,0 +1,87 @@
+import React, { useState, useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+import styles from './Tabs.module.css';
+
+const Tabs = ({ tabs }) => {
+ const [selectedTab, setSelectedTab] = useState(0);
+ const tabRefs = useRef([]);
+
+ useEffect(() => {
+ tabRefs.current = tabRefs.current.slice(0, tabs.length);
+ }, [tabs]);
+
+ const handleKeyDown = (event, index) => {
+ const tabCount = tabs.length;
+ let newIndex = index;
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ newIndex = (index - 1 + tabCount) % tabCount;
+ break;
+ case 'ArrowRight':
+ newIndex = (index + 1) % tabCount;
+ break;
+ case 'Home':
+ newIndex = 0;
+ break;
+ case 'End':
+ newIndex = tabCount - 1;
+ break;
+ default:
+ return;
+ }
+
+ event.preventDefault();
+ setSelectedTab(newIndex);
+ tabRefs.current[newIndex]?.focus();
+ };
+
+ return (
+
+
+ {tabs.map((tab, index) => (
+ (tabRefs.current[index] = el)}
+ id={`tab-${index}`}
+ aria-selected={selectedTab === index}
+ aria-controls={`panel-${index}`}
+ className={`${styles.tabButton} ${
+ selectedTab === index ? styles.selectedTab : ''
+ }`}
+ onClick={() => setSelectedTab(index)}
+ onKeyDown={e => handleKeyDown(e, index)}
+ tabIndex={selectedTab === index ? 0 : -1}
+ >
+ {tab.label}
+
+ ))}
+
+ {tabs.map((tab, index) => (
+
+ {tab.content}
+
+ ))}
+
+ );
+};
+
+Tabs.propTypes = {
+ tabs: PropTypes.arrayOf(
+ PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ content: PropTypes.node.isRequired
+ })
+ ).isRequired
+};
+
+export default Tabs;
diff --git a/client/components/common/Tabs/Tabs.module.css b/client/components/common/Tabs/Tabs.module.css
new file mode 100644
index 000000000..e4ec79741
--- /dev/null
+++ b/client/components/common/Tabs/Tabs.module.css
@@ -0,0 +1,53 @@
+.tab-list {
+ margin-bottom: 1.5rem;
+ display: flex;
+ gap: 1rem;
+ border-bottom: 1px solid #dee2e6;
+ padding-bottom: 2px;
+}
+
+.tab-button {
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: #6c757d;
+ cursor: pointer;
+ font-size: 1.1rem;
+ padding: 0.75rem 1.25rem;
+ position: relative;
+ transition: all 0.2s ease-in-out;
+ margin-bottom: -2px;
+}
+
+.tab-button:hover {
+ color: #0d6efd;
+ background-color: #f8f9fa;
+ border-radius: 4px 4px 0 0;
+}
+
+.tab-button:focus-visible {
+ outline: 2px solid #0d6efd;
+ outline-offset: -2px;
+ border-radius: 4px 4px 0 0;
+}
+
+.tab-button span {
+ position: relative;
+ z-index: 1;
+}
+
+.selected-tab {
+ border-bottom-color: #0d6efd;
+ color: #0d6efd;
+}
+
+.tab-panel {
+ padding: 1rem 0.5rem;
+ transition: opacity 0.15s ease-in-out;
+ opacity: 1;
+}
+
+.tab-panel[hidden] {
+ display: none;
+ opacity: 0;
+}
diff --git a/client/components/common/Tabs/index.js b/client/components/common/Tabs/index.js
new file mode 100644
index 000000000..bc6749b1b
--- /dev/null
+++ b/client/components/common/Tabs/index.js
@@ -0,0 +1 @@
+export { default } from './Tabs';
diff --git a/client/static/index.css b/client/static/index.css
index 57b65f4cc..7fd485bcd 100644
--- a/client/static/index.css
+++ b/client/static/index.css
@@ -26,6 +26,8 @@
/* Miscellaneous */
--default-gray: #d2d5d9;
--border-gray: #d2d5d9;
+ --dark-gray: #495057;
+ --darkest-gray: #24292f;
--border-light-blue: #b5cfec;
--border-dark-blue: #517dbc;
--border-light-purple: #d29fff;
@@ -96,6 +98,7 @@
/* Shadows */
--button-shadow: rgba(103, 171, 197, 0.5);
+ --box-shadow-gray: rgba(0, 0, 0, 0.05);
/* Font sizes */
--default-font-size: 1rem;
diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js
index a7e800ce6..1868bbca5 100644
--- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js
+++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageBaseMock.js
@@ -1,4 +1,8 @@
-export default (testPlanReportAtBrowserQuery, existingTestPlanReportsQuery) => [
+export default (
+ testPlanReportAtBrowserQuery,
+ existingTestPlanReportsQuery,
+ getUpdateEventsQuery
+) => [
{
request: {
query: testPlanReportAtBrowserQuery,
@@ -110,5 +114,16 @@ export default (testPlanReportAtBrowserQuery, existingTestPlanReportsQuery) => [
oldTestPlanVersions: []
}
}
+ },
+ {
+ request: {
+ query: getUpdateEventsQuery,
+ variables: {}
+ },
+ result: {
+ data: {
+ updateEvents: []
+ }
+ }
}
];
diff --git a/client/tests/__mocks__/GraphQLMocks/index.js b/client/tests/__mocks__/GraphQLMocks/index.js
index a9b9e9f94..74e906f22 100644
--- a/client/tests/__mocks__/GraphQLMocks/index.js
+++ b/client/tests/__mocks__/GraphQLMocks/index.js
@@ -3,6 +3,7 @@ import { TEST_PLAN_REPORT_AT_BROWSER_QUERY } from '@components/common/AssignTest
import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '@components/TestPlanReportStatusDialog/queries';
import { EXISTING_TEST_PLAN_REPORTS } from '@components/AddTestToQueueWithConfirmation/queries';
import { ME_QUERY } from '@components/App/queries';
+import { GET_UPDATE_EVENTS } from '@components/ReportRerun/queries';
import TestQueuePageAdminNotPopulatedMock from './TestQueuePageAdminNotPopulatedMock';
import TestQueuePageTesterNotPopulatedMock from './TestQueuePageTesterNotPopulatedMock';
@@ -18,7 +19,8 @@ export const TEST_QUEUE_PAGE_TESTER_NOT_POPULATED_MOCK_DATA =
export const TEST_QUEUE_PAGE_BASE_MOCK_DATA = TestQueuePageBaseMock(
TEST_PLAN_REPORT_AT_BROWSER_QUERY,
- EXISTING_TEST_PLAN_REPORTS
+ EXISTING_TEST_PLAN_REPORTS,
+ GET_UPDATE_EVENTS
);
export const TEST_PLAN_REPORT_STATUS_DIALOG_MOCK_DATA =
diff --git a/client/tests/e2e/AtVersions.e2e.test.js b/client/tests/e2e/AtVersions.e2e.test.js
index 799152779..ab1a07272 100644
--- a/client/tests/e2e/AtVersions.e2e.test.js
+++ b/client/tests/e2e/AtVersions.e2e.test.js
@@ -20,7 +20,7 @@ describe('AT Version UI', () => {
await page.type('.modal-body .form-group:nth-child(1) input', '2020.0.1');
await page.type('.modal-body .form-group:nth-child(2) input', '01-01-2000');
await page.click('.modal-footer button ::-p-text(Add Version)');
- await page.waitForNetworkIdle({ idleTime: 5000 });
+ await page.waitForSelector('::-p-text(Successfully Added Assistive Technology Version)');
await page.click('.modal-footer button ::-p-text(Ok)');
await page.waitForSelector('.at-versions-container option:nth-child(2) ::-p-text(2020.0.1)');
const optionValue = await page.$eval('.at-versions-container option:nth-child(2)', option => option.value);
@@ -32,7 +32,7 @@ describe('AT Version UI', () => {
}
await page.type('.modal-body .form-group:nth-child(1) input', '99.0.99');
await page.click('.modal-footer button ::-p-text(Save)');
- await page.waitForNetworkIdle({ idleTime: 5000 });
+ await page.waitForSelector('::-p-text(Successfully Updated Assistive Technology Version)');
await page.click('.modal-footer button ::-p-text(Ok)');
await page.waitForSelector('.at-versions-container option ::-p-text(99.0.99)');
await page.select('.at-versions-container select', optionValue);
diff --git a/client/tests/e2e/ReportRerun.e2e.test.js b/client/tests/e2e/ReportRerun.e2e.test.js
new file mode 100644
index 000000000..ee6fa9ae2
--- /dev/null
+++ b/client/tests/e2e/ReportRerun.e2e.test.js
@@ -0,0 +1,115 @@
+import getPage from '../util/getPage';
+
+describe('Report Rerun tab', () => {
+ const switchToReportRerunTab = async page => {
+ await page.waitForSelector(
+ 'button[role="tab"] span::-p-text(Automated Report Updates)'
+ );
+ await page.click(
+ 'button[role="tab"] span::-p-text(Automated Report Updates)'
+ );
+ await page.waitForSelector('[role="tabpanel"]:not([hidden])');
+ };
+
+ it('shows different content based on user role', async () => {
+ await getPage({ role: 'tester', url: '/test-queue' }, async page => {
+ await switchToReportRerunTab(page);
+
+ const rerunDashboardExists = await page.evaluate(() => {
+ return document.querySelector('.rerun-dashboard') !== null;
+ });
+ expect(rerunDashboardExists).toBe(false);
+ });
+
+ await getPage({ role: 'admin', url: '/test-queue' }, async page => {
+ await switchToReportRerunTab(page);
+
+ const rerunOpportunities = await page.evaluate(() => {
+ return Array.from(document.querySelectorAll('.rerun-opportunity')).map(
+ el => ({
+ botName: el.querySelector('.bot-name').textContent,
+ hasStartButton: el.querySelector('.rerun-button') !== null
+ })
+ );
+ });
+
+ expect(rerunOpportunities.length).toBeGreaterThan(0);
+ rerunOpportunities.forEach(opportunity => {
+ expect(opportunity.botName).toBeTruthy();
+ expect(opportunity.hasStartButton).toBe(true);
+ });
+ });
+ });
+
+ it('displays update events table', async () => {
+ await getPage({ role: 'admin', url: '/test-queue' }, async page => {
+ await switchToReportRerunTab(page);
+
+ const tableStructure = await page.evaluate(() => {
+ const table = document.querySelector(
+ 'table[aria-label="Test plan rerun events history"]'
+ );
+ if (!table) return null;
+
+ const headers = Array.from(table.querySelectorAll('th')).map(
+ th => th.textContent
+ );
+ const hasTimeColumn = headers.includes('Time');
+ const hasMessageColumn = headers.includes('Message');
+
+ return { hasTimeColumn, hasMessageColumn };
+ });
+
+ expect(tableStructure).toEqual({
+ hasTimeColumn: true,
+ hasMessageColumn: true
+ });
+ });
+ });
+
+ it('handles rerun action for specific bot version', async () => {
+ await getPage({ role: 'admin', url: '/test-queue' }, async page => {
+ await switchToReportRerunTab(page);
+
+ const startUpdateResult = await page.evaluate(() => {
+ const nvdaBot = Array.from(
+ document.querySelectorAll('.rerun-opportunity')
+ ).find(el =>
+ el.querySelector('.bot-name').textContent.includes('NVDA Bot')
+ );
+
+ if (!nvdaBot) return { found: false };
+
+ const button = nvdaBot.querySelector('.rerun-button');
+ if (!button) return { found: false };
+
+ const testPlanCount =
+ nvdaBot.querySelector('.plan-count-number').textContent;
+ button.click();
+
+ return {
+ found: true,
+ testPlanCount
+ };
+ });
+
+ expect(startUpdateResult.found).toBe(true);
+
+ await page.waitForFunction(() => {
+ const table = document.querySelector(
+ 'table[aria-label="Test plan rerun events history"]'
+ );
+
+ if (!table) return false;
+
+ const rows = table.querySelectorAll('tbody tr');
+ if (rows.length === 0) return false;
+ const firstRowMessage =
+ rows[0].querySelector('.message-cell')?.textContent || '';
+ return firstRowMessage.includes(
+ 'Created 1 re-run collection job for NVDA'
+ );
+ });
+ });
+ });
+});
diff --git a/client/tests/e2e/TestReview.e2e.test.js b/client/tests/e2e/TestReview.e2e.test.js
index 8a6101666..6649f4d49 100644
--- a/client/tests/e2e/TestReview.e2e.test.js
+++ b/client/tests/e2e/TestReview.e2e.test.js
@@ -70,6 +70,8 @@ describe('Test Review page', () => {
url: '/data-management'
},
async page => {
+ page.setDefaultTimeout(60000);
+
await text(page, 'h1 ::-p-text(Data Management)');
const latestAlertVersionLink = await page.evaluateHandle(() => {
diff --git a/client/tests/e2e/snapshots/saved/_data-management.html b/client/tests/e2e/snapshots/saved/_data-management.html
index 5e27cfb21..b944c4ef1 100644
--- a/client/tests/e2e/snapshots/saved/_data-management.html
+++ b/client/tests/e2e/snapshots/saved/_data-management.html
@@ -274,7 +274,7 @@