Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 0-level assertion priorities on TestRun page #863

Merged
merged 20 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
69245f8
Update client/resources
howard-e Dec 7, 2023
619a5fe
Update graphql-schema.js and testsResolver
howard-e Dec 7, 2023
0c099f2
Update assertionResultsResolver.js
howard-e Dec 7, 2023
51ae9a5
Update import-tests script to include assertion.assertionExceptions (…
howard-e Dec 7, 2023
7c464db
Update getMetrics and queries to be aware of 'excluded' assertions fo…
howard-e Dec 7, 2023
bde9219
Update import branch for testing
howard-e Dec 7, 2023
c7d57fa
Update import branch for testing
howard-e Dec 7, 2023
5c4ec6a
Remove 'exclude' property check reliance on frontend
howard-e Dec 13, 2023
7de158b
Revert graphql fragment usage
howard-e Dec 14, 2023
3cbe58a
Fix missing renderableContent in anon viewer query
howard-e Dec 14, 2023
635cf65
Rename instances of Command.settings to Command.atOperatingMode in gr…
howard-e Dec 14, 2023
318b7f4
Include migration to update existing test plan versions using the v2 …
howard-e Jan 22, 2024
0d5d165
Merge branch 'main' into support-0-level-priority
stalgiag Jan 22, 2024
a26a572
Move atOperatingMode setting to TestsService, getTests
stalgiag Jan 22, 2024
62ecce2
Missing comma
stalgiag Jan 22, 2024
45d3ed6
Merge branch 'main' into support-0-level-priority
howard-e Jan 30, 2024
af9a5b0
Update comments with reasoning for filtering out assertions that can …
howard-e Feb 6, 2024
3d1fbcb
Update convertAssertionPriority usage
howard-e Feb 6, 2024
942223d
Revert test branch being used back to master
howard-e Feb 6, 2024
bba8ad4
Break out in-line filter into function sufficiently support readabili…
howard-e Feb 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions client/components/TestRenderer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ const TestRenderer = ({
}

for (let i = 0; i < scenarioResults.length; i++) {
const {
let {
output,
assertionResults,
unexpectedBehaviors,
Expand All @@ -258,15 +258,23 @@ const TestRenderer = ({
if (output) commands[i].atOutput.value = output;
commands[i].atOutput.highlightRequired = highlightRequired;

// Required because assertionResults can now be returned without an id if there is a 0-priority exception
// applied
assertionResults = assertionResults.filter(el => !!el.id);

for (let j = 0; j < assertionResults.length; j++) {
const assertionResult = assertionResults[j];
const { highlightRequired } = assertionResult;
const { passed, highlightRequired, assertion } =
assertionResults[j];

commands[i].assertions[j].result = assertionResult.passed
let assertionForCommandIndex = commands[i].assertions.findIndex(
({ description }) => description === assertion?.text
);
commands[i].assertions[assertionForCommandIndex].result = passed
? 'pass'
: 'fail';

commands[i].assertions[j].highlightRequired = highlightRequired;
commands[i].assertions[
assertionForCommandIndex
].highlightRequired = highlightRequired;
}

if (unexpectedBehaviors && unexpectedBehaviors.length) {
Expand Down
19 changes: 13 additions & 6 deletions client/components/TestRun/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,12 @@ const TestRun = () => {

// process assertion results
for (let j = 0; j < assertions.length; j++) {
const { result, highlightRequired } = assertions[j];
const { description, result, highlightRequired } =
assertions[j];
const assertionResult = {
...scenarioResult.assertionResults[j],
...scenarioResult.assertionResults.find(
({ assertion: { text } }) => text === description
),
passed: result === 'pass'
};
assertionResults.push(
Expand Down Expand Up @@ -638,10 +641,14 @@ const TestRun = () => {
id,
output: output,
unexpectedBehaviors: unexpectedBehaviors,
assertionResults: assertionResults.map(({ id, passed }) => ({
id,
passed
}))
assertionResults: assertionResults
// All assertions are always being passed from the TestRenderer results, but in the instance where
// there is a 0-priority exception, and id won't be provided and that cannot be saved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this taking advantage of a side effect or is this the strategy used for communicating that an assertion is excluded? It seems easy to misunderstand or forget. I would expect them to be passed prefiltered but I might be missing something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been some time now, so I'll have to look back into why exactly it was done that way. But from what I can remember, the TestRenderer still technically requires the index of that excluded assertion to properly determine which assertions are which, but at some point. It definitely had to be removed before saving and those excluded ones had no ids. That was a part of fulfilling this request with 5c4ec6a.

Is this taking advantage of a side effect or is this the strategy used for communicating that an assertion is excluded?

So seems to be a bit of both.

This point in the code looks like the safest place to filter them out, as the single point before being passed to the server/db. The comment could be much clearer as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay. The background is helpful. I'll leave it to you as to whether you want to change the comment or address it any other way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in af9a5b0

.filter(el => !!el.id)
.map(({ id, passed }) => ({
id,
passed
}))
})
);

Expand Down
3 changes: 3 additions & 0 deletions client/components/TestRun/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ export const FIND_OR_CREATE_TEST_RESULT_MUTATION = gql`
id
title
phase
updatedAt
gitSha
testPageUrl
testPlan {
Expand Down Expand Up @@ -806,6 +807,7 @@ export const SAVE_TEST_RESULT_MUTATION = gql`
id
title
phase
updatedAt
gitSha
testPageUrl
testPlan {
Expand Down Expand Up @@ -1090,6 +1092,7 @@ export const SUBMIT_TEST_RESULT_MUTATION = gql`
id
title
phase
updatedAt
gitSha
testPageUrl
testPlan {
Expand Down
59 changes: 55 additions & 4 deletions client/resources/aria-at-test-io-format.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ class CommandsInput {
commands.push(command);
commandsAndSettings.push({
command,
commandId,
presentationNumber: Number(presentationNumber),
settings: _atMode,
settingsText: assistiveTech.settings?.[_atMode]?.screenText || 'default mode active',
settingsInstructions: assistiveTech.settings?.[_atMode]?.instructions || [
Expand Down Expand Up @@ -767,6 +769,9 @@ class BehaviorInput {

const { commandsAndSettings } = commandsInput.getCommands({ task: json.task }, mode);

// Use to determine assertionExceptions
const commandsInfo = json.commandsInfo?.[at.key];

return new BehaviorInput({
behavior: {
description: titleInput.title(),
Expand All @@ -778,7 +783,31 @@ class BehaviorInput {
setupScriptDescription: json.setup_script_description,
setupTestPage: json.setupTestPage,
assertionResponseQuestion: json.assertionResponseQuestion,
commands: commandsAndSettings,
commands: commandsAndSettings.map(cs => {
const foundCommandInfo = commandsInfo?.find(
c =>
cs.commandId === c.command &&
cs.presentationNumber === c.presentationNumber &&
cs.settings === c.settings
);
if (!foundCommandInfo || !foundCommandInfo.assertionExceptions) return cs;

// Only works for v2
let assertionExceptions = json.output_assertions.map(each => each.assertionId);
foundCommandInfo.assertionExceptions.split(' ').forEach(each => {
let [priority, assertionId] = each.split(':');
const index = assertionExceptions.findIndex(each => each === assertionId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.prototype.indexOf would be a bit more readable here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed


priority = Number(priority);
assertionExceptions[index] = priority;
Comment on lines +801 to +802
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under the assumption that "fewer mutable bindings is better", these statements could be combined to allow swapping the earlier let for a const.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

});
// Preserve default priority or update with exception
assertionExceptions = assertionExceptions.map((each, index) =>
isNaN(each) ? json.output_assertions[index].priority : each
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't the members of assertions string values? Returning each from a filter predicate suggests otherwise.

);

return { ...cs, assertionExceptions };
}),
assertions: (json.output_assertions ? json.output_assertions : []).map(assertion => {
// Tuple array [ priorityNumber, assertionText ]
if (Array.isArray(assertion)) {
Expand All @@ -788,7 +817,7 @@ class BehaviorInput {
};
}

// { assertionId, priority, assertionStatement, assertionPhrase, refIds, commandInfo, tokenizedAssertionStatements }
// { assertionId, priority, assertionStatement, assertionPhrase, refIds, tokenizedAssertionStatements }
return {
priority: assertion.priority,
assertion:
Expand All @@ -815,7 +844,7 @@ class BehaviorInput {
* @param {UnexpectedInput} data.unexpectedInput
*/
static fromCollectedTestCommandsKeysUnexpected(
{ info, target, instructions, assertions },
{ info, target, instructions, assertions, commands },
{ commandsInput, keysInput, unexpectedInput }
) {
// v1:info.task, v2: info.testId | v1:target.mode, v2:target.at.settings
Expand All @@ -834,7 +863,28 @@ class BehaviorInput {
specificUserInstruction: instructions.raw || instructions.instructions,
setupScriptDescription: target.setupScript ? target.setupScript.description : '',
setupTestPage: target.setupScript ? target.setupScript.name : undefined,
commands: commandsAndSettings,
commands: commandsAndSettings.map(cs => {
const foundCommandInfo = commands.find(
c => cs.commandId === c.id && cs.settings === c.settings
);
if (!foundCommandInfo || !foundCommandInfo.assertionExceptions) return cs;

// Only works for v2
let assertionExceptions = assertions.map(each => each.assertionId);
foundCommandInfo.assertionExceptions.forEach(each => {
let { priority, assertionId } = each;
const index = assertionExceptions.findIndex(each => each === assertionId);

priority = Number(priority);
assertionExceptions[index] = priority;
});
// Preserve default priority or update with exception
assertionExceptions = assertionExceptions.map((each, index) =>
isNaN(each) ? assertions[index].priority : each
);

return { ...cs, assertionExceptions };
}),
assertions: assertions.map(
({ priority, expectation, assertionStatement, tokenizedAssertionStatements }) => {
let assertion = tokenizedAssertionStatements
Expand Down Expand Up @@ -1187,6 +1237,7 @@ export class TestRunInputOutput {
description: command.settings,
text: command.settingsText,
instructions: command.settingsInstructions,
assertionExceptions: command.assertionExceptions,
},
atOutput: {
highlightRequired: false,
Expand Down
44 changes: 36 additions & 8 deletions client/resources/aria-at-test-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export function instructionDocument(resultState, hooks) {
const resultUnexpectedBehavior = resultStateCommand.unexpected;

const {
commandSettings: { description: settings, text: settingsText },
commandSettings: { description: settings, text: settingsText, assertionExceptions },
} = resultStateCommand;

return {
Expand Down Expand Up @@ -248,7 +248,16 @@ export function instructionDocument(resultState, hooks) {
}?`,
},
assertions: [
...assertions.map(bind(assertionResult, commandIndex)),
...assertions
// Ignore assertion if level 0 priority exception found for assertion's command
.filter((each, index) => (assertionExceptions ? assertionExceptions[index] !== 0 : each))
.map(each =>
assertionResult(
commandIndex,
each,
assertions.findIndex(e => e === each)
)
),
...additionalAssertions.map(bind(additionalAssertionResult, commandIndex)),
],
unexpectedBehaviors: {
Expand Down Expand Up @@ -742,10 +751,20 @@ function resultsTableDocument(state) {
header: [
'Test result: ',
state.commands.some(
({ assertions, additionalAssertions, unexpected }) =>
[...assertions, ...additionalAssertions].some(
({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS
) || unexpected.behaviors.some(({ checked }) => checked)
({
assertions,
additionalAssertions,
unexpected,
commandSettings: { assertionExceptions },
}) =>
[
// Ignore assertion if level 0 priority exception found for assertion's command
...assertions.filter((each, index) =>
assertionExceptions ? assertionExceptions[index] !== 0 : each
),
...additionalAssertions,
].some(({ priority, result }) => priority === 1 && result !== CommonResultMap.PASS) ||
unexpected.behaviors.some(({ checked }) => checked)
)
? 'FAIL'
: 'PASS',
Expand All @@ -758,11 +777,20 @@ function resultsTableDocument(state) {
details: 'Details',
},
commands: state.commands.map(command => {
const allAssertions = [...command.assertions, ...command.additionalAssertions];
const {
commandSettings: { assertionExceptions },
} = command;
const allAssertions = [
// Ignore assertion if level 0 priority exception found for assertion's command
...command.assertions.filter((each, index) =>
assertionExceptions ? assertionExceptions[index] !== 0 : each
),
...command.additionalAssertions,
];

let passingAssertions = ['No passing assertions.'];
let failingAssertions = ['No failing assertions.'];
let unexpectedBehaviors = ['No unexpect behaviors.'];
let unexpectedBehaviors = ['No unexpected behaviors.'];

if (allAssertions.some(({ result }) => result === CommonResultMap.PASS)) {
passingAssertions = allAssertions
Expand Down
1 change: 1 addition & 0 deletions client/resources/at-commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export class commandsAPI {
return {
value,
key,
settings: mode,
};
})
);
Expand Down
8 changes: 8 additions & 0 deletions server/graphql-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,14 @@ const graphqlSchema = gql`
A human-readable version of the command, such as "Control+Alt+Down"
"""
text: String!
"""
The AT mode this command may be getting ran in, such as quickNavOn,
browseMode, etc.
The same command can be ran during the same test, but in a different
mode.
"""
# TODO: Add link to list of known AT modes
atOperatingMode: String
}

"""
Expand Down
26 changes: 23 additions & 3 deletions server/resolvers/ScenarioResult/assertionResultsResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,31 @@ const convertAssertionPriority = require('../helpers/convertAssertionPriority');
const assertionResultsResolver = (scenarioResult, { priority }) => {
if (!priority) return scenarioResult.assertionResults;

return scenarioResult.assertionResults.filter(
assertionResult =>
return scenarioResult.assertionResults.filter(assertionResult => {
if (assertionResult.assertion?.assertionExceptions?.length) {
const scenarioSettings = scenarioResult.scenario.settings;
const scenarioCommandId = scenarioResult.scenario.commandIds[0];

const foundException =
assertionResult.assertion.assertionExceptions.find(
exception =>
exception.settings === scenarioSettings &&
exception.commandId === scenarioCommandId
);

if (foundException) {
return (
convertAssertionPriority(foundException.priority) ===
convertAssertionPriority(priority)
);
}
}

return (
convertAssertionPriority(assertionResult.assertion.priority) ===
convertAssertionPriority(priority)
);
);
});
};

module.exports = assertionResultsResolver;
18 changes: 14 additions & 4 deletions server/resolvers/TestPlanRunOperations/createTestResultSkeleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,26 @@ const createTestResultSkeleton = ({
id: scenarioResultId,
scenarioId: scenario.id,
output: null,
assertionResults: test.assertions.map(assertion => {
return {
assertionResults: test.assertions
// Filter out assertionResults which were marked with a 0-priority exception
.filter(assertion => {
return !assertion.assertionExceptions?.some(
e =>
scenario.commands.find(
c =>
c.id === e.commandId &&
c.atOperatingMode === e.settings
) && e.priority === 'EXCLUDE'
);
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment doesn't seem to acknowledge the first half of the filter expression, making me wonder whether I understand what's going on here. Would it be accurate to say, "Filter out assertionResults for the current scenario which were marked with a 0-priority exception"?

Stylistically, I think that the closure nesting is reaching a point where it interferes with readability. While the in-line documentation you've suggested helps, breaking this logic out into a standalone function would afford us more room to describe what's going on, reduce the cognitive load (by formalizing the inputs), and limit distraction for folks seeking to understand createTestResultSkeleton at a high level.

/**
 * Determine whether a given assertion belongs to a given scenario and includes
 * at least one exception with a given priority.
 *
 * @param {string} priority
 * @param {Scenario} scenario
 * @param {Assertion} assertion
 */
const hasExceptionWithPriority = (priority, scenario, assertion) => {
  return assertion.assertionExceptions?.some(
      e =>
          scenario.commands.find(
              c =>
                  c.id === e.commandId &&
                  c.atOperatingMode === e.settings
          ) && e.priority === priority
  );
};

Then invoking like this:

.filter(assertion => !hasExceptionWithPriority('EXCLUDE', scenario, assertion))

I'm only sharing something concrete as a means to demonstrate my point; I'm not too concerned with the specific factoring. For instance, a more purpose-built function would simplify the callsite even further (though it might be difficult to name it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this. I was able to take your suggestion and only apply minor changes, thanks! Done in bba8ad4

.map(assertion => ({
id: createAssertionResultId(
scenarioResultId,
assertion.id
),
assertionId: assertion.id,
passed: null
};
}),
})),
unexpectedBehaviors: null
};
})
Expand Down
3 changes: 2 additions & 1 deletion server/resolvers/TestPlanVersion/testsResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const testsResolver = parentRecord => {
id: commandKVs[0].key,
text: `${commandKVs[0].value}${
screenText ? ` (${screenText})` : ''
}`
}`,
atOperatingMode: scenario.settings
};
}
return { id: '', text: '' };
Expand Down
Loading
Loading