diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000000..17161e32e08 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 00000000000..02e4254378e --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 00000000000..698b01a42c3 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 00000000000..f80f74f8e1f --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/protocol-designer/cypress/e2e/mixSettings.cy.ts b/protocol-designer/cypress/e2e/mixSettings.cy.ts new file mode 100644 index 00000000000..85252d762b2 --- /dev/null +++ b/protocol-designer/cypress/e2e/mixSettings.cy.ts @@ -0,0 +1,61 @@ +import '../support/commands.ts'; // Importing the custom commands file +import { + Actions, + Verifications, + runMixSetup, +} from '../support/mixSetting' +import { UniversalActions } from '../support/universalActions' +import { TestFilePath, getTestFile } from '../support/testFiles' +import { + // verifyOldProtocolModal, + verifyImportProtocolPage, +} from '../support/import' + +describe('Redesigned Mixing Steps - Happy Path', () => { + beforeEach(() => { + cy.visit('/') + cy.closeAnalyticsModal() + const protocol = getTestFile(TestFilePath.DoItAllV8) + cy.importProtocol(protocol.path) + verifyImportProtocolPage(protocol) + + // NOTE: vv make this chunk better// + cy.contains("Edit protocol").click() + cy.contains("Protocol steps").click() + cy.get('[id="AddStepButton"]').contains("Add Step").click() + cy.verifyOverflowBtn() + }); + + + it('It should verify the working function of every permutation of mix checkboxes', () => { + const steps: Array = [ + Actions.SelectMix, + UniversalActions.Snapshot, + Verifications.PartOne, + Actions.SelectLabware, + Actions.SelectWellInputField, + Verifications.WellSelectPopout, + UniversalActions.Snapshot, + Actions.Save, + Actions.EnterVolume, + Actions.EnterMixReps, + Actions.SelectTipHandling, + UniversalActions.Snapshot, + Actions.Continue, + Verifications.PartTwoAsp, + Actions.AspirateFlowRate, + Actions.AspWellOrder, + Verifications.AspWellOrder, + // Actions.Dispense, + // Verifications.PartTwoDisp, + + ] + + runMixSetup(steps) + // cy.contains('Primary order').closest('div') + }); +}); + +/* +NEED TO ADD RENAME +*/ \ No newline at end of file diff --git a/protocol-designer/cypress/support/commands.ts b/protocol-designer/cypress/support/commands.ts index 697ddfa32f5..3dfc586999c 100644 --- a/protocol-designer/cypress/support/commands.ts +++ b/protocol-designer/cypress/support/commands.ts @@ -32,6 +32,8 @@ declare global { verifyCreateNewPage: () => Cypress.Chainable togglePreWetTip: () => Cypress.Chainable mixaspirate: () => Cypress.Chainable + clickConfirm: () => Cypress.Chainable + verifyOverflowBtn: () => Cypress.Chainable } } } @@ -49,6 +51,12 @@ export const content = { appSettings: 'App Info', privacy: 'Privacy', shareSessions: 'Share analytics with Opentrons', + move: "Move", + transfer: "Transfer", + mix: "Mix", + pause: "Pause", + heaterShaker: "Heater-shaker", + thermocyler: "Thermocycler", } export const locators = { @@ -104,11 +112,12 @@ Cypress.Commands.add('verifyCreateNewHeader', () => { // Home Page Cypress.Commands.add('verifyHomePage', () => { cy.contains(content.welcome) + cy.get(locators.privacyPolicy).should('exist').and('be.visible') + cy.get(locators.eula).should('exist').and('be.visible') + cy.contains(locators.confirm).click() cy.contains('button', locators.createProtocol).should('be.visible') cy.contains('label', locators.importProtocol).should('be.visible') cy.getByTestId(locators.settingsDataTestid).should('be.visible') - cy.get(locators.privacyPolicy).should('exist').and('be.visible') - cy.get(locators.eula).should('exist').and('be.visible') }) Cypress.Commands.add('clickCreateNew', () => { @@ -122,6 +131,10 @@ Cypress.Commands.add('closeAnalyticsModal', () => { .click({ force: true }) }) +Cypress.Commands.add('clickConfirm', () => { + cy.contains(locators.confirm).click() +}) + // Header Import Cypress.Commands.add('importProtocol', (protocolFilePath: string) => { cy.contains(locators.import).click() @@ -158,6 +171,15 @@ Cypress.Commands.add('verifySettingsPage', () => { .should('be.visible') }) +Cypress.Commands.add('verifyOverflowBtn', () => { + cy.contains(content.move).should('exist').should('be.visible') + cy.contains(content.transfer).should('exist').should('be.visible') + cy.contains(content.mix).should('exist').should('be.visible') + cy.contains(content.pause).should('exist').should('be.visible') + cy.contains(content.heaterShaker).should('exist').should('be.visible') + cy.contains(content.thermocyler).should('exist').should('be.visible') +}) + /// ///////////////////////////////////////////////////////////////// // Legacy Code Section // This code is deprecated and should be removed @@ -183,9 +205,7 @@ Cypress.Commands.add('openFilePage', () => { // Pipette Page Actions // -Cypress.Commands.add( - 'choosePipettes', - (leftPipetteSelector, rightPipetteSelector) => { +Cypress.Commands.add('choosePipettes', (leftPipetteSelector, rightPipetteSelector) => { cy.get('[id="PipetteSelect_left"]').click() cy.get(leftPipetteSelector).click() cy.get('[id="PipetteSelect_right"]').click() diff --git a/protocol-designer/cypress/support/mixSetting.ts b/protocol-designer/cypress/support/mixSetting.ts new file mode 100644 index 00000000000..80fe00fd2d9 --- /dev/null +++ b/protocol-designer/cypress/support/mixSetting.ts @@ -0,0 +1,262 @@ +import { contains } from 'cypress/types/jquery' +import { executeUniversalAction, UniversalActions } from './universalActions' +import { isEnumValue } from './utils' + +export enum Actions { + Confirm = 'Confirm', + Continue = 'Continue', + GoBack = 'Go back', + Back = 'Back', + Save = 'Save', + Edit = 'Edit', + SelectMix = "Select Mix", + SelectLabware = 'Select on deck labware', + SelectWellInputField = 'Select wells', + EnterVolume = 'Enter a valid volume to mix', + EnterMixReps = 'Enter number of repetions to mix', + SelectTipHandling = 'Select how/if tips should be picked up for each mix', + AspirateFlowRate = 'Select aspirate settings', + Dispense = 'Select dispnse settings', + AspWellOrder = 'Open well aspirate well order pop out', + EditWellOrder = 'Edit well order selects', + +} + +export enum Verifications { + PartOne = 'Verify Part 1, the configuration of mix settings, and check continue button', + PartTwoAsp = 'Verify Part 2, the configuration of asp settings and check go back and save button', + PartTwoDisp = 'Verify Part 2, the configuration of disp settings and check go back and save button', + WellSelectPopout = 'Verify labware image and available wells', + AspWellOrder = 'Verify pop out for well order during aspirate', + AspMixTipPos = 'Verify pop out for mix tip position durin aspirate', +} + +export enum Content { + Move = 'Move', + Transfer = 'Transfer', + Mix = 'Mix', + Pause = 'Pause', + HeaterShaker = 'Heater-shaker', + Thermocyler = 'Thermocycler', + Pipette = 'Pipette', + Tiprack = 'Tiprack', + Labware = 'Labware', + SelectWells = 'Select wells', + VolumePerWell = 'Volume per well', + MixRepetitions = 'Mix repetitions', + TipHandling = 'Tip handling', + TipDropLocation = 'Tip drop location', + ChooseOption = 'Choose option', + Reservoir = 'Axygen 1 Well Reservoir 90 mL', + WellPlate = 'Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt', + PartOne = 'Part 1 / 2', + PartTwo = 'Part 2 / 2', + WellSelectTitle = 'Select wells using a Flex 1-Channel 1000 μL', + ClickAndDragWellSelect = "Click and drag to select wells", + PipettePreselect = 'Flex 1-Channel 1000 μL', + TiprackPreselect = 'Opentrons Flex 96 Tip Rack 1000 µL', + BeforeEveryAsp = 'Before every aspirate', + OnceAtStartStep = 'Once at the start of step', + PerSourceWell = 'Per source well', + PerDestWell = 'Per destination well', + Never = 'Never', + WasteChute = 'Waste chute', + AspFlowRate = 'Aspirate flow rate', + AspWellOrder = 'Aspirate well order', + MixTipPosition = 'Mix tip position', + AdvancedPipSettings = 'Advanced pipetting settings', + Delay = 'Delay', + DelayDuration = 'Delay Duration', + DispFlowRate = "Dispense flow rate", + Blowout = 'Blowout', + TouchTip = 'Touch tip', + TopBottomLeRi = 'Top to bottom, Left to right', + EditWellOrder = 'Edit well order', + WellOrderDescrip = 'Change how the robot moves from well to well.', + PrimaryOrder = 'Primary order', + TopToBottom = 'Top to bottom', + BottomToTop = 'Bottom to top', + LeftToRight = 'Left to right', + RightToLeft = 'Right to left', + Then = 'then', + SecondaryOrder = 'Secondary order', + Cancel = 'Cancel', +} + +export enum Locators { + Continue = 'button:contains("Continue")', + GoBack = 'button:contains("Go back")', + Back = 'button:contains("Back")', + WellInputField = '[name="wells"]', + Save = 'button:contains("Save")', + OneWellReservoirImg = '[data-wellname="A1"]', + Volume = '[name="volume"]', + MixReps = '[name="times"]', + Aspirate = 'button:contains("Aspirate")', + Dispense = 'button:contains("Dispense")', + AspFlowRateInput = '[name="aspirate_flowRate"]', + AspWellOrder = '[class="Flex-sc-1qhp8l7-0 ListButton___StyledFlex-sc-1lmhs3v-0 bToGfF bdMeyp"]', + ResetToDefault = 'button:contains("Reset to default")', + PrimaryOrderDropdown = 'div[tabindex="0"].sc-bqWxrE jKLbYH iFjNDq', + CancelWellOrder = '[class="SecondaryButton-sc-1opt1t9-0 kjpcRL"]', +} + + +const executeAction = (action: Actions | UniversalActions): void => { + if (isEnumValue([UniversalActions], [action])) { + executeUniversalAction(action as UniversalActions) + return + } + + + + switch (action) { + case Actions.SelectMix: + cy.get('button').contains('Mix').click() + break + case Actions.SelectLabware: + cy.contains(Content.ChooseOption).should('be.visible').click() + cy.contains(Content.Reservoir).should('be.visible').click() + break + case Actions.SelectWellInputField: + cy.get(Locators.WellInputField).should('be.visible').click() + break + case Actions.EnterVolume: + cy.get(Locators.Volume).should('exist').type('100') + break + case Actions.EnterMixReps: + cy.get(Locators.MixReps).should('exist').type('5') + break + case Actions.SelectTipHandling: + cy.contains(Content.BeforeEveryAsp).should('exist').should('be.visible').click() + cy.contains(Content.OnceAtStartStep).should('exist').should('be.visible') + cy.contains(Content.PerSourceWell).should('exist').should('be.visible') + cy.contains(Content.PerDestWell).should('exist').should('be.visible') + cy.contains(Content.Never).should('exist').should('be.visible') + cy.contains(Content.OnceAtStartStep).click() + break + case Actions.AspirateFlowRate: + cy.get(Locators.Aspirate).should('exist').should('be.visible').click() + cy.get(Locators.AspFlowRateInput).should('exist') + cy.get(Locators.AspFlowRateInput).type('{selectAll}, {backspace}, 100') + break + case Actions.AspWellOrder: + cy.contains(Content.TopBottomLeRi).should('exist').should('be.visible') + cy.get(Locators.AspWellOrder).click() + break + case Actions.Dispense: + cy.get(Locators.Dispense).should('exist').should('be.visible').click() + break + // case Actions.FlowRateWarning: + // break + case Actions.Save: + cy.get(Locators.Save).should('exist').should('be.visible').click() + break + case Actions.Back: + cy.get(Locators.Back).should('exist').should('be.visible').click() + break + case Actions.Continue: + cy.get(Locators.Continue).should('exist').should('be.visible').click({force: true}) + break + default: + throw new Error(`Unrecognized action: ${action as string}`) + } +} + +const verifyStep = (verification: Verifications): void => { + switch (verification) { + case Verifications.PartOne: + cy.contains(Content.PartOne).should('exist').should('be.visible') + cy.contains(Content.Mix).should('exist').should('be.visible') + cy.contains(Content.Pipette).should('exist').should('be.visible') + cy.contains(Content.PipettePreselect).should('exist').should('be.visible') + cy.contains(Content.Tiprack).should('exist').should('be.visible') + cy.contains(Content.TiprackPreselect).should('exist').should('be.visible') + cy.contains(Content.Labware).should('exist').should('be.visible') + cy.contains(Content.SelectWells).should('exist').should('be.visible') + cy.contains(Content.VolumePerWell).should('exist').should('be.visible') + cy.contains(Content.MixRepetitions).should('exist').should('be.visible') + cy.contains(Content.TipHandling).should('exist').should('be.visible') + cy.contains(Content.TipDropLocation).should('exist').should('be.visible') + cy.contains(Content.WasteChute).should('exist').should('be.visible') + cy.get(Locators.Continue).should('exist').should('be.visible') + break + case Verifications.WellSelectPopout: + cy.contains(Content.WellSelectTitle).should('exist').should('be.visible') + cy.contains(Content.ClickAndDragWellSelect).should('exist').should('be.visible') + cy.get(Locators.OneWellReservoirImg).should('exist').should('be.visible') + cy.get(Locators.Save).should('exist').should('be.visible') + cy.get(Locators.Back).should('exist').should('be.visible') + break + case Verifications.PartTwoAsp: + cy.contains(Content.PartTwo).should('exist').should('be.visible') + cy.contains(Content.Mix).should('exist').should('be.visible') + cy.get(Locators.Aspirate).should('exist').should('be.visible') + cy.contains(Content.AspFlowRate).should('exist').should('be.visible') + cy.contains(Content.AspWellOrder).should('exist').should('be.visible') + cy.contains(Content.MixTipPosition).should('exist').should('be.visible') + cy.contains(Content.AdvancedPipSettings).should('exist').should('be.visible') + cy.contains(Content.Delay).should('exist').should('be.visible') + cy.get(Locators.Back).should('exist').should('be.visible') + cy.get(Locators.Save).should('exist').should('be.visible') + break + case Verifications.PartTwoDisp: + cy.contains(Content.PartTwo).should('exist').should('be.visible') + cy.contains(Content.Mix).should('exist').should('be.visible') + cy.get(Locators.Dispense).should('exist').should('be.visible') + cy.contains(Content.DispFlowRate).should('exist').should('be.visible') + cy.contains(Content.AdvancedPipSettings).should('exist').should('be.visible') + cy.contains(Content.Delay).should('exist').should('be.visible') + cy.contains(Content.Blowout).should('exist').should('be.visible') + cy.contains(Content.TouchTip).should('exist').should('be.visible') + break + case Verifications.AspWellOrder: + cy.contains(Content.EditWellOrder).should('exist').should('be.visible') + cy.contains(Content.WellOrderDescrip).should('exist').should('be.visible') + cy.contains(Content.PrimaryOrder).should('exist').should('be.visible') + cy.contains(Content.TopToBottom).should('exist').should('be.visible').click() + cy.contains(Content.BottomToTop).should('exist').should('be.visible') + cy.contains(Content.LeftToRight).should('exist').should('be.visible') + cy.contains(Content.RightToLeft).should('exist').should('be.visible') + cy.contains(Content.BottomToTop).should('exist').should('be.visible').click() + cy.contains(Content.Then).should('exist').should('be.visible') + cy.contains(Content.SecondaryOrder).should('exist').should('be.visible') + cy.contains(Content.LeftToRight).should('exist').should('be.visible').click() + cy.contains(Content.RightToLeft).should('exist').should('be.visible').click() + cy.get(Locators.ResetToDefault).click() + cy.contains(Content.TopToBottom).should('exist').should('be.visible') + cy.contains(Content.LeftToRight).should('exist').should('be.visible') + cy.get(Locators.CancelWellOrder).should('exist').should('be.visible') + cy.get(Locators.Save).should('exist').should('be.visible') + break + // case Verifications.MixTipPos: + // break + // case Verifications.FlowRateRangeWarning: + // break + default: + throw new Error( + `Unrecognized verification: ${verification as Verifications}` + ) + } +} + +export const runMixSetup = ( + steps: Array +): void => { + const enumsToCheck = [Actions, Verifications, UniversalActions] + + if (!isEnumValue(enumsToCheck, steps)) { + throw new Error('One or more steps are unrecognized.') + } + + steps.forEach(step => { + if (isEnumValue([Actions], step)) { + executeAction(step as Actions) + } else if (isEnumValue([Verifications], step)) { + verifyStep(step as Verifications) + } else if (isEnumValue([UniversalActions], step)) { + executeAction(step as UniversalActions) + } + }) +} +