Skip to content

Commit 1a7a223

Browse files
authored
Merge pull request #1556 from w3c/development
Create October 2, 2025 Release Includes the following changes: * #1538, which addresses #1513 * #1499, which addresses #1377 * #1549 * #1550 * #1551 * #1553 * #1554
2 parents 6279b60 + 57787fb commit 1a7a223

34 files changed

+908
-141
lines changed

client/components/AddTestToQueueWithConfirmation/index.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,18 @@ function AddTestToQueueWithConfirmation({
132132
<>
133133
Choose how the report for {at?.name} and {browser?.name} will be
134134
generated. Add it to the queue so it can be assigned to a tester at a
135-
later time or start running automated response collection with{' '}
135+
later time or start running automated response collection with&nbsp;
136136
{getBotUsernameFromAtBrowser(at, browser)}.
137137
</>
138138
) : (
139139
<>
140-
Successfully added <b>{testPlanVersion?.title}</b> for{' '}
140+
Successfully added&nbsp;
141+
<b>{`${testPlanVersion?.title} ${testPlanVersion?.versionString}`}</b>
142+
&nbsp;for&nbsp;
141143
<b>
142144
{at?.name} and {browser?.name}
143-
</b>{' '}
144-
to the Test Queue.
145+
</b>
146+
&nbsp;to the Test Queue.
145147
</>
146148
);
147149

client/components/AddTestToQueueWithConfirmation/queries.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { gql } from '@apollo/client';
22

33
export const SCHEDULE_COLLECTION_JOB_MUTATION = gql`
4-
mutation ScheduleCollectionJob($testPlanReportId: ID!) {
5-
scheduleCollectionJob(testPlanReportId: $testPlanReportId) {
4+
mutation ScheduleCollectionJob($testPlanReportId: ID!, $isRerun: Boolean) {
5+
scheduleCollectionJob(
6+
testPlanReportId: $testPlanReportId
7+
isRerun: $isRerun
8+
) {
69
id
710
status
811
}

client/components/BotRunTestStatusList/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const BotRunTestStatusList = ({ testPlanReportId }) => {
9696
)}
9797
<li>
9898
<ReportStatusDot status={REPORT_STATUSES.TESTS_COMPLETE} />
99-
{testCountString(COMPLETED, 'Completed')}
99+
{testCountString(COMPLETED, 'Executed')}
100100
</li>
101101
<li>
102102
<ReportStatusDot status={REPORT_STATUSES.TESTS_QUEUED} />
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useMemo, useRef, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useApolloClient } from '@apollo/client';
4+
import { Button } from 'react-bootstrap';
5+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6+
import { faFileImport } from '@fortawesome/free-solid-svg-icons';
7+
import useConfirmationModal from '../../../hooks/useConfirmationModal';
8+
import { useTriggerLoad } from '../../common/LoadingStatus';
9+
import BasicModal from '../../common/BasicModal';
10+
import { SCHEDULE_COLLECTION_JOB_MUTATION } from '../../AddTestToQueueWithConfirmation/queries';
11+
import { TEST_QUEUE_PAGE_QUERY } from '../../TestQueue/queries';
12+
import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '../../TestPlanReportStatusDialog/queries';
13+
14+
const StartBotRunButton = ({ testPlanReport, onChange }) => {
15+
const client = useApolloClient();
16+
const { triggerLoad } = useTriggerLoad();
17+
const { showConfirmationModal, hideConfirmationModal } =
18+
useConfirmationModal();
19+
// Synchronize the state of the action with the modal UI
20+
const [isActionPending, setIsActionPending] = useState(false);
21+
// Ref guard to prevent concurrent actions
22+
const isConfirmingRef = useRef(false);
23+
24+
const atLatestAutomationSupportedVersion = useMemo(() => {
25+
const versions = testPlanReport?.at?.atVersions || [];
26+
const supported = versions.filter(v => v.supportedByAutomation);
27+
if (!supported.length) return null;
28+
return supported.reduce((latest, v) =>
29+
new Date(v.releasedAt) > new Date(latest.releasedAt) ? v : latest
30+
);
31+
}, [testPlanReport]);
32+
33+
// Shorten the AT name to the first word
34+
// specifically needed for "VoiceOver for macOS"
35+
const shortenedAtName = useMemo(() => {
36+
return testPlanReport.at.name.split(' ')[0];
37+
}, [testPlanReport]);
38+
39+
const onStart = () => {
40+
const title = `Start ${testPlanReport.at.name} Bot Run`;
41+
const content = (
42+
<>
43+
<p>
44+
This will run the bot using {testPlanReport.at.name}{' '}
45+
{atLatestAutomationSupportedVersion?.name ??
46+
'(no automation-supported version found)'}
47+
.
48+
</p>
49+
</>
50+
);
51+
52+
const onConfirm = async () => {
53+
if (isConfirmingRef.current || isActionPending) return;
54+
isConfirmingRef.current = true;
55+
setIsActionPending(true);
56+
// Immediately reflect disabled/label in the modal UI
57+
showConfirmationModal(renderModal());
58+
try {
59+
await triggerLoad(async () => {
60+
await client.mutate({
61+
mutation: SCHEDULE_COLLECTION_JOB_MUTATION,
62+
variables: { testPlanReportId: testPlanReport.id },
63+
refetchQueries: [
64+
TEST_QUEUE_PAGE_QUERY,
65+
TEST_PLAN_REPORT_STATUS_DIALOG_QUERY
66+
],
67+
awaitRefetchQueries: true
68+
});
69+
}, 'Scheduling Collection Job');
70+
hideConfirmationModal();
71+
if (onChange) await onChange();
72+
} finally {
73+
setIsActionPending(false);
74+
isConfirmingRef.current = false;
75+
}
76+
};
77+
78+
const renderModal = () => (
79+
<BasicModal
80+
show
81+
title={title}
82+
content={content}
83+
staticBackdrop={true}
84+
handleClose={() => hideConfirmationModal()}
85+
useOnHide={true}
86+
actions={[
87+
{
88+
label:
89+
isConfirmingRef.current || isActionPending
90+
? 'Starting...'
91+
: 'Start',
92+
onClick: onConfirm,
93+
testId: 'confirm-start-bot-run',
94+
disabled: isConfirmingRef.current || isActionPending
95+
}
96+
]}
97+
/>
98+
);
99+
100+
showConfirmationModal(renderModal());
101+
};
102+
103+
return (
104+
<Button variant="secondary" onClick={onStart}>
105+
<FontAwesomeIcon icon={faFileImport} />
106+
{`Start ${shortenedAtName} Bot Run`}
107+
</Button>
108+
);
109+
};
110+
111+
StartBotRunButton.propTypes = {
112+
testPlanReport: PropTypes.shape({
113+
id: PropTypes.string.isRequired,
114+
at: PropTypes.shape({
115+
name: PropTypes.string.isRequired,
116+
atVersions: PropTypes.arrayOf(
117+
PropTypes.shape({
118+
name: PropTypes.string.isRequired,
119+
releasedAt: PropTypes.string.isRequired,
120+
supportedByAutomation: PropTypes.bool
121+
})
122+
)
123+
}).isRequired
124+
}).isRequired,
125+
onChange: PropTypes.func
126+
};
127+
128+
export default StartBotRunButton;

client/components/ManageBotRunDialog/WithButton.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const ManageBotRunDialogWithButton = ({
1111
testPlanReportId,
1212
runnableTestsLength,
1313
testers,
14-
onChange
14+
onChange,
15+
me
1516
}) => {
1617
const [showDialog, setShowDialog] = useState(false);
1718

@@ -32,6 +33,7 @@ const ManageBotRunDialogWithButton = ({
3233
show={showDialog}
3334
setShow={setShowDialog}
3435
testers={testers}
36+
me={me}
3537
testPlanReportId={testPlanReportId}
3638
runnableTestsLength={runnableTestsLength}
3739
onChange={async () => {
@@ -49,7 +51,8 @@ ManageBotRunDialogWithButton.propTypes = {
4951
testPlanReportId: PropTypes.string.isRequired,
5052
runnableTestsLength: PropTypes.number.isRequired,
5153
testers: PropTypes.arrayOf(UserPropType).isRequired,
52-
onChange: PropTypes.func.isRequired
54+
onChange: PropTypes.func.isRequired,
55+
me: UserPropType
5356
};
5457

5558
export default ManageBotRunDialogWithButton;

client/components/ManageBotRunDialog/index.jsx

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ import ViewLogsButton from './ViewLogsButton';
1818
import { TestPlanRunPropType, UserPropType } from '../common/proptypes';
1919
import { COLLECTION_JOB_STATUS } from '../../utils/collectionJobStatus';
2020
import styles from './ManageBotRunDialog.module.css';
21+
import { evaluateAuth } from '../../utils/evaluateAuth';
22+
import { ASSIGN_TESTER_MUTATION } from '../common/AssignTesterDropdown/queries';
23+
import { Button } from 'react-bootstrap';
2124

2225
const ManageBotRunDialog = ({
2326
testPlanReportId,
2427
runnableTestsLength,
2528
testPlanRun,
2629
testers,
30+
me,
2731
show,
2832
setShow,
2933
onChange
@@ -69,6 +73,10 @@ const ManageBotRunDialog = ({
6973
[testers, testPlanReportAssignedTestersQuery]
7074
);
7175

76+
const { isAdmin, isTester } = evaluateAuth(me);
77+
78+
const [assignTester] = useMutation(ASSIGN_TESTER_MUTATION);
79+
7280
const isBotRunFinished = useMemo(() => {
7381
const status = collectionJobQuery?.collectionJobByTestPlanRunId?.status;
7482
if (!status) return false;
@@ -83,18 +91,7 @@ const ManageBotRunDialog = ({
8391
}, [collectionJobQuery]);
8492

8593
const actions = useMemo(() => {
86-
return [
87-
{
88-
component: AssignTesterDropdown,
89-
props: {
90-
testPlanReportId: testPlanReportId,
91-
testPlanRun: testPlanRun,
92-
possibleTesters: possibleReassignees,
93-
label: 'Assign To ...',
94-
disabled: !isBotRunFinished,
95-
onChange
96-
}
97-
},
94+
const baseActions = [
9895
{
9996
component: ViewLogsButton,
10097
props: {
@@ -135,12 +132,62 @@ const ManageBotRunDialog = ({
135132
}
136133
}
137134
];
135+
136+
if (isAdmin) {
137+
return [
138+
{
139+
component: AssignTesterDropdown,
140+
props: {
141+
testPlanReportId: testPlanReportId,
142+
testPlanRun: testPlanRun,
143+
possibleTesters: possibleReassignees,
144+
label: 'Assign To ...',
145+
disabled: !isBotRunFinished,
146+
onChange
147+
}
148+
},
149+
...baseActions
150+
];
151+
}
152+
153+
if (isTester) {
154+
const AssignSelfButton = props => (
155+
<Button {...props} data-testid="assign-self-bot-run">
156+
Assign Yourself
157+
</Button>
158+
);
159+
return [
160+
{
161+
component: AssignSelfButton,
162+
props: {
163+
variant: 'primary',
164+
disabled: !isBotRunFinished,
165+
onClick: async () => {
166+
await assignTester({
167+
variables: {
168+
testReportId: testPlanReportId,
169+
testerId: me.id,
170+
testPlanRunId: testPlanRun.id
171+
}
172+
});
173+
await onChange();
174+
}
175+
}
176+
},
177+
...baseActions
178+
];
179+
}
180+
181+
return baseActions;
138182
}, [
139183
testPlanReportId,
140184
testPlanRun,
141185
possibleReassignees,
142186
onChange,
143-
collectionJobQuery
187+
collectionJobQuery,
188+
isAdmin,
189+
isTester,
190+
isBotRunFinished
144191
]);
145192

146193
const deleteConfirmationContent = (
@@ -204,6 +251,7 @@ ManageBotRunDialog.propTypes = {
204251
show: PropTypes.bool.isRequired,
205252
setShow: PropTypes.func.isRequired,
206253
testers: PropTypes.arrayOf(UserPropType).isRequired,
254+
me: UserPropType,
207255
testPlanReportId: PropTypes.string.isRequired,
208256
runnableTestsLength: PropTypes.number.isRequired,
209257
onChange: PropTypes.func.isRequired

client/components/TestQueue/Actions.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import BasicThemedModal from '../common/BasicThemedModal';
2222
import { evaluateAuth } from '../../utils/evaluateAuth';
2323
import { TEST_PLAN_REPORT_STATUS_DIALOG_QUERY } from '../TestPlanReportStatusDialog/queries';
2424
import ManageBotRunDialogWithButton from '@components/ManageBotRunDialog/WithButton';
25+
import StartBotRunButton from '@components/ManageBotRunDialog/StartBotRunButton';
2526
import {
2627
TestPlanPropType,
2728
TestPlanReportPropType,
@@ -265,12 +266,19 @@ const Actions = ({
265266
))}
266267
</Dropdown.Menu>
267268
</Dropdown>
268-
{isAdmin && assignedBotRun && (
269+
{(isAdmin || isTester) && assignedBotRun && (
269270
<ManageBotRunDialogWithButton
270271
testPlanRun={assignedBotRun}
271272
testPlanReportId={testPlanReport.id}
272273
runnableTestsLength={testPlanReport.runnableTestsLength}
273274
testers={testers}
275+
me={me}
276+
onChange={triggerUpdate}
277+
/>
278+
)}
279+
{(isAdmin || isTester) && !assignedBotRun && (
280+
<StartBotRunButton
281+
testPlanReport={testPlanReport}
274282
onChange={triggerUpdate}
275283
/>
276284
)}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import ProgressBar from '../common/ProgressBar';
4+
import ReportStatusSummary from '../common/ReportStatusSummary';
5+
import { useTestPlanReportPercentComplete } from '../../hooks/useTestPlanReportPercentComplete';
6+
import { TestPlanVersionPropType } from '../common/proptypes';
7+
8+
const RowStatus = ({ testPlanVersion, testPlanReport, shouldPoll }) => {
9+
const { percentComplete } = useTestPlanReportPercentComplete(
10+
testPlanReport.id,
11+
shouldPoll ? 2000 : null
12+
);
13+
14+
const progress =
15+
typeof percentComplete === 'number'
16+
? percentComplete
17+
: testPlanReport.percentComplete || 0;
18+
19+
return (
20+
<>
21+
<ProgressBar progress={progress} decorative />
22+
<ReportStatusSummary
23+
testPlanVersion={testPlanVersion}
24+
testPlanReport={{ ...testPlanReport, percentComplete: progress }}
25+
fromTestQueue
26+
/>
27+
</>
28+
);
29+
};
30+
31+
RowStatus.propTypes = {
32+
testPlanVersion: TestPlanVersionPropType.isRequired,
33+
testPlanReport: PropTypes.shape({
34+
id: PropTypes.string.isRequired,
35+
percentComplete: PropTypes.number
36+
}).isRequired,
37+
shouldPoll: PropTypes.bool
38+
};
39+
40+
export default RowStatus;

0 commit comments

Comments
 (0)