diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 860c0848ff8..c53c589b231 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -99,7 +99,7 @@ export interface RunsLinks { } export interface RunCommandLink { - current: CommandLinkNoMeta + lastCompleted: CommandLinkNoMeta } export interface CommandLinkNoMeta { diff --git a/api/docs/v2/new_examples.rst b/api/docs/v2/new_examples.rst index 28490e03135..1aae3b633d0 100644 --- a/api/docs/v2/new_examples.rst +++ b/api/docs/v2/new_examples.rst @@ -383,7 +383,7 @@ Opentrons electronic pipettes can do some things that a human cannot do with a p location=3) p300 = protocol.load_instrument( instrument_name="p300_single", - mount="right", + mount="left", tip_racks=[tiprack_1]) p300.pick_up_tip() @@ -442,13 +442,13 @@ This protocol dispenses diluent to all wells of a Corning 96-well plate. Next, i source = reservoir.wells()[i] row = plate.rows()[i] - # transfer 30 µL of source to first well in column - pipette.transfer(30, source, row[0], mix_after=(3, 25)) + # transfer 30 µL of source to first well in column + pipette.transfer(30, source, row[0], mix_after=(3, 25)) - # dilute the sample down the column - pipette.transfer( - 30, row[:11], row[1:], - mix_after=(3, 25)) + # dilute the sample down the column + pipette.transfer( + 30, row[:11], row[1:], + mix_after=(3, 25)) .. tab:: OT-2 @@ -474,7 +474,7 @@ This protocol dispenses diluent to all wells of a Corning 96-well plate. Next, i location=4) p300 = protocol.load_instrument( instrument_name="p300_single", - mount="right", + mount="left", tip_racks=[tiprack_1, tiprack_2]) # Dispense diluent p300.distribute(50, reservoir["A12"], plate.wells()) @@ -483,16 +483,15 @@ This protocol dispenses diluent to all wells of a Corning 96-well plate. Next, i for i in range(8): # save the source well and destination column to variables source = reservoir.wells()[i] - source = reservoir.wells()[i] row = plate.rows()[i] - # transfer 30 µL of source to first well in column - p300.transfer(30, source, row[0], mix_after=(3, 25)) + # transfer 30 µL of source to first well in column + p300.transfer(30, source, row[0], mix_after=(3, 25)) - # dilute the sample down the column - p300.transfer( - 30, row[:11], row[1:], - mix_after=(3, 25)) + # dilute the sample down the column + p300.transfer( + 30, row[:11], row[1:], + mix_after=(3, 25)) Notice here how the code sample loops through the rows and uses slicing to distribute the diluent. For information about these features, see the Loops and Air Gaps examples above. See also, the :ref:`tutorial-commands` section of the Tutorial. diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 09f726de767..dfdb0eec56f 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -11,6 +11,7 @@ SphericalSegment, ConicalFrustum, CuboidalFrustum, + SquaredConeSegment, ) @@ -127,6 +128,15 @@ def _volume_from_height_spherical( return volume +def _volume_from_height_squared_cone( + target_height: float, segment: SquaredConeSegment +) -> float: + """Find the volume given a height within a squared cone segment.""" + heights = segment.height_to_volume_table.keys() + best_fit_height = min(heights, key=lambda x: abs(x - target_height)) + return segment.height_to_volume_table[best_fit_height] + + def _height_from_volume_circular( volume: float, total_frustum_height: float, @@ -197,7 +207,17 @@ def _height_from_volume_spherical( return height +def _height_from_volume_squared_cone( + target_volume: float, segment: SquaredConeSegment +) -> float: + """Find the height given a volume within a squared cone segment.""" + volumes = segment.volume_to_height_table.keys() + best_fit_volume = min(volumes, key=lambda x: abs(x - target_volume)) + return segment.volume_to_height_table[best_fit_volume] + + def _get_segment_capacity(segment: WellSegment) -> float: + section_height = segment.topHeight - segment.bottomHeight match segment: case SphericalSegment(): return _volume_from_height_spherical( @@ -205,7 +225,6 @@ def _get_segment_capacity(segment: WellSegment) -> float: radius_of_curvature=segment.radiusOfCurvature, ) case CuboidalFrustum(): - section_height = segment.topHeight - segment.bottomHeight return _volume_from_height_rectangular( target_height=section_height, bottom_length=segment.bottomYDimension, @@ -215,13 +234,14 @@ def _get_segment_capacity(segment: WellSegment) -> float: total_frustum_height=section_height, ) case ConicalFrustum(): - section_height = segment.topHeight - segment.bottomHeight return _volume_from_height_circular( target_height=section_height, total_frustum_height=section_height, bottom_radius=(segment.bottomDiameter / 2), top_radius=(segment.topDiameter / 2), ) + case SquaredConeSegment(): + return _volume_from_height_squared_cone(section_height, segment) case _: # TODO: implement volume calculations for truncated circular and rounded rectangular segments raise NotImplementedError( @@ -275,6 +295,8 @@ def height_at_volume_within_section( top_width=section.topXDimension, top_length=section.topYDimension, ) + case SquaredConeSegment(): + return _height_from_volume_squared_cone(target_volume_relative, section) case _: raise NotImplementedError( "Height from volume calculation not yet implemented for this well shape." @@ -309,6 +331,8 @@ def volume_at_height_within_section( top_width=section.topXDimension, top_length=section.topYDimension, ) + case SquaredConeSegment(): + return _volume_from_height_squared_cone(target_height_relative, section) case _: # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 697e4a14e3a..69d9feaf524 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -257,6 +257,22 @@ def get_current_command(self) -> Optional[CommandPointer]: """Get the "current" command, if any.""" return self._protocol_engine.state_view.commands.get_current() + def get_most_recently_finalized_command(self) -> Optional[CommandPointer]: + """Get the most recently finalized command, if any.""" + most_recently_finalized_command = ( + self._protocol_engine.state_view.commands.get_most_recently_finalized_command() + ) + return ( + CommandPointer( + command_id=most_recently_finalized_command.command.id, + command_key=most_recently_finalized_command.command.key, + created_at=most_recently_finalized_command.command.createdAt, + index=most_recently_finalized_command.index, + ) + if most_recently_finalized_command + else None + ) + def get_command_slice( self, cursor: Optional[int], length: int, include_fixit_commands: bool ) -> CommandSlice: diff --git a/api/src/opentrons/util/logging_config.py b/api/src/opentrons/util/logging_config.py index 944f4d3d5ed..42a32501576 100644 --- a/api/src/opentrons/util/logging_config.py +++ b/api/src/opentrons/util/logging_config.py @@ -125,7 +125,7 @@ def _buildroot_config(level_value: int) -> Dict[str, Any]: }, "sensor": { "class": "logging.handlers.RotatingFileHandler", - "formatter": "basic", + "formatter": "message_only", "filename": sensor_log_filename, "maxBytes": 1000000, "level": logging.DEBUG, diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index 53ae23d07e2..78dc2b852a6 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -49,7 +49,7 @@ "install_probe": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", "loose_detach": "Loosen screws and detach ", "move_gantry_to_front": "Move gantry to front", - "must_detach_mounting_plate": "You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.", + "must_detach_mounting_plate": "You must detach the mounting plate and reattach the z-axis carriage before using other pipettes. We do not recommend exiting this process before completion.", "name_and_volume_detected": "{{name}} Pipette Detected", "next": "next", "ninety_six_channel": "{{ninetySix}} pipette", diff --git a/app/src/assets/videos/error-recovery/Gripper_Release.webm b/app/src/assets/videos/error-recovery/Gripper_Release.webm new file mode 100644 index 00000000000..a3ba721fd70 Binary files /dev/null and b/app/src/assets/videos/error-recovery/Gripper_Release.webm differ diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts index cd16d2467b6..4c5486eb6e7 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' +import { useSelector } from 'react-redux' import { RUN_STATUS_IDLE, @@ -14,6 +15,7 @@ import { useTrackEvent, } from '/app/redux/analytics' import { useTrackProtocolRunEvent } from '/app/redux-resources/analytics' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import { useIsHeaterShakerInProtocol } from '/app/organisms/ModuleCard/hooks' import { isAnyHeaterShakerShaking } from '../../../RunHeaderModalContainer/modals' import { @@ -24,6 +26,8 @@ import { import type { IconName } from '@opentrons/components' import type { BaseActionButtonProps } from '..' +import type { State } from '/app/redux/types' +import type { StepKey } from '/app/redux/protocol-runs' interface UseButtonPropertiesProps extends BaseActionButtonProps { isProtocolNotReady: boolean @@ -42,7 +46,6 @@ interface UseButtonPropertiesProps extends BaseActionButtonProps { export function useActionButtonProperties({ isProtocolNotReady, runStatus, - missingSetupSteps, robotName, runId, confirmAttachment, @@ -66,6 +69,9 @@ export function useActionButtonProperties({ const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) const trackEvent = useTrackEvent() + const missingSetupSteps = useSelector((state: State) => + getMissingSetupSteps(state, runId) + ) let buttonText = '' let handleButtonClick = (): void => {} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts index 4bf28bc049f..d0506c55153 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts @@ -1,17 +1,21 @@ +import { useSelector } from 'react-redux' import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' import { useConditionalConfirm } from '@opentrons/components' import { useIsHeaterShakerInProtocol } from '/app/organisms/ModuleCard/hooks' import { isAnyHeaterShakerShaking } from '../modals' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import type { UseConditionalConfirmResult } from '@opentrons/components' import type { RunStatus, AttachedModule } from '@opentrons/api-client' import type { ConfirmMissingStepsModalProps } from '../modals' +import type { State } from '/app/redux/types' +import type { StepKey } from '/app/redux/protocol-runs' interface UseMissingStepsModalProps { runStatus: RunStatus | null attachedModules: AttachedModule[] - missingSetupSteps: string[] + runId: string handleProceedToRunClick: () => void } @@ -30,12 +34,14 @@ export type UseMissingStepsModalResult = export function useMissingStepsModal({ attachedModules, runStatus, - missingSetupSteps, + runId, handleProceedToRunClick, }: UseMissingStepsModalProps): UseMissingStepsModalResult { const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) - + const missingSetupSteps = useSelector((state: State) => + getMissingSetupSteps(state, runId) + ) const shouldShowHSConfirm = isHeaterShakerInProtocol && !isHeaterShakerShaking && diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx index 978efdbab48..8203e126a2d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx @@ -12,11 +12,27 @@ import { TYPOGRAPHY, Modal, } from '@opentrons/components' +import { + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + MODULE_SETUP_STEP_KEY, + ROBOT_CALIBRATION_STEP_KEY, +} from '/app/redux/protocol-runs' +import type { StepKey } from '/app/redux/protocol-runs' + +const STEP_KEY_TO_I18N_KEY = { + [LPC_STEP_KEY]: 'applied_labware_offsets', + [LABWARE_SETUP_STEP_KEY]: 'labware_placement', + [LIQUID_SETUP_STEP_KEY]: 'liquids', + [MODULE_SETUP_STEP_KEY]: 'module_setup', + [ROBOT_CALIBRATION_STEP_KEY]: 'robot_calibration', +} export interface ConfirmMissingStepsModalProps { onCloseClick: () => void onConfirmClick: () => void - missingSteps: string[] + missingSteps: StepKey[] } export const ConfirmMissingStepsModal = ( props: ConfirmMissingStepsModalProps @@ -41,7 +57,7 @@ export const ConfirmMissingStepsModal = ( missingSteps: new Intl.ListFormat('en', { style: 'short', type: 'conjunction', - }).format(missingSteps.map(step => t(step))), + }).format(missingSteps.map(step => t(STEP_KEY_TO_I18N_KEY[step]))), })} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts index 17d81c1f18e..48eda0ebfa5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts @@ -62,7 +62,6 @@ export function useRunHeaderModalContainer({ runStatus, runRecord, attachedModules, - missingSetupSteps, protocolRunControls, runErrors, }: UseRunHeaderModalContainerProps): UseRunHeaderModalContainerResult { @@ -102,7 +101,7 @@ export function useRunHeaderModalContainer({ const missingStepsModalUtils = useMissingStepsModal({ attachedModules, runStatus, - missingSetupSteps, + runId, handleProceedToRunClick, }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx index 9cc357d0565..e82d58cb75e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx @@ -30,6 +30,7 @@ vi.mock('react-router-dom') vi.mock('@opentrons/react-api-client') vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/runs') +vi.mock('/app/redux/protocol-runs') vi.mock('../RunHeaderModalContainer') vi.mock('../RunHeaderBannerContainer') vi.mock('../RunHeaderContent') @@ -51,7 +52,6 @@ describe('ProtocolRunHeader', () => { robotName: MOCK_ROBOT, runId: MOCK_RUN_ID, makeHandleJumpToStep: vi.fn(), - missingSetupSteps: [], } vi.mocked(useNavigate).mockReturnValue(mockNavigate) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx index c6d33879be9..40375135db9 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx @@ -35,7 +35,6 @@ export interface ProtocolRunHeaderProps { robotName: string runId: string makeHandleJumpToStep: (index: number) => () => void - missingSetupSteps: string[] } export function ProtocolRunHeader( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 0f48d0bb833..8e10948795a 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -30,6 +31,7 @@ import { } from '/app/resources/deck_configuration/utils' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { useRobot, useIsFlex } from '/app/redux-resources/robots' +import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { useMostRecentCompletedAnalysis, @@ -40,6 +42,15 @@ import { useModuleCalibrationStatus, useProtocolAnalysisErrors, } from '/app/resources/runs' +import { + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + updateRunSetupStepsComplete, + getMissingSetupSteps, +} from '/app/redux/protocol-runs' import { SetupLabware } from './SetupLabware' import { SetupLabwarePositionCheck } from './SetupLabwarePositionCheck' import { SetupRobotCalibration } from './SetupRobotCalibration' @@ -49,51 +60,29 @@ import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' -const ROBOT_CALIBRATION_STEP_KEY = 'robot_calibration_step' as const -const MODULE_SETUP_KEY = 'module_setup_step' as const -const LPC_KEY = 'labware_position_check_step' as const -const LABWARE_SETUP_KEY = 'labware_setup_step' as const -const LIQUID_SETUP_KEY = 'liquid_setup_step' as const - -export type StepKey = - | typeof ROBOT_CALIBRATION_STEP_KEY - | typeof MODULE_SETUP_KEY - | typeof LPC_KEY - | typeof LABWARE_SETUP_KEY - | typeof LIQUID_SETUP_KEY - -export type MissingStep = - | 'applied_labware_offsets' - | 'labware_placement' - | 'liquids' - -export type MissingSteps = MissingStep[] - -export const initialMissingSteps = (): MissingSteps => [ - 'applied_labware_offsets', - 'labware_placement', - 'liquids', -] +import type { Dispatch, State } from '/app/redux/types' +import type { StepKey } from '/app/redux/protocol-runs' interface ProtocolRunSetupProps { protocolRunHeaderRef: React.RefObject | null robotName: string runId: string - setMissingSteps: (missingSteps: MissingSteps) => void - missingSteps: MissingSteps } export function ProtocolRunSetup({ protocolRunHeaderRef, robotName, runId, - setMissingSteps, - missingSteps, }: ProtocolRunSetupProps): JSX.Element | null { const { t, i18n } = useTranslation('protocol_setup') + const dispatch = useDispatch() const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis + const { + orderedSteps, + orderedApplicableSteps, + } = useRequiredSetupStepsInOrder({ runId, protocolAnalysis }) const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? []) const robot = useRobot(robotName) @@ -130,39 +119,6 @@ export function ProtocolRunSetup({ const isMissingModule = missingModuleIds.length > 0 - const stepsKeysInOrder = - protocolAnalysis != null - ? [ - ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_KEY, - LPC_KEY, - LABWARE_SETUP_KEY, - LIQUID_SETUP_KEY, - ] - : [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY] - - const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { - if (protocolAnalysis == null) { - return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY - } - - if ( - protocolAnalysis.modules.length === 0 && - protocolAnalysis.liquids.length === 0 - ) { - return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY - } - - if (protocolAnalysis.modules.length === 0) { - return stepKey !== MODULE_SETUP_KEY - } - - if (protocolAnalysis.liquids.length === 0) { - return stepKey !== LIQUID_SETUP_KEY - } - return true - }) - const liquids = protocolAnalysis?.liquids ?? [] const hasLiquids = liquids.length > 0 const hasModules = protocolAnalysis != null && modules.length > 0 @@ -179,26 +135,10 @@ export function ProtocolRunSetup({ ? t('install_modules', { count: modules.length }) : t('no_deck_hardware_specified') - const [ - labwareSetupComplete, - setLabwareSetupComplete, - ] = React.useState(false) - const [liquidSetupComplete, setLiquidSetupComplete] = React.useState( - false + const missingSteps = useSelector( + (state: State): StepKey[] => getMissingSetupSteps(state, runId) ) - React.useEffect(() => { - if ((robotProtocolAnalysis || storedProtocolAnalysis) && !hasLiquids) { - setLiquidSetupComplete(true) - } - }, [robotProtocolAnalysis, storedProtocolAnalysis, hasLiquids]) - if ( - !hasLiquids && - protocolAnalysis != null && - missingSteps.includes('liquids') - ) { - setMissingSteps(missingSteps.filter(step => step !== 'liquids')) - } - const [lpcComplete, setLpcComplete] = React.useState(false) + if (robot == null) { return null } @@ -216,8 +156,8 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} nextStep={ - targetStepKeyInOrder[ - targetStepKeyInOrder.findIndex( + orderedApplicableSteps[ + orderedApplicableSteps.findIndex( v => v === ROBOT_CALIBRATION_STEP_KEY ) + 1 ] @@ -240,11 +180,11 @@ export function ProtocolRunSetup({ incompleteElement: null, }, }, - [MODULE_SETUP_KEY]: { + [MODULE_SETUP_STEP_KEY]: { stepInternals: ( { - setExpandedStepKey(LPC_KEY) + setExpandedStepKey(LPC_STEP_KEY) }} robotName={robotName} runId={runId} @@ -256,7 +196,7 @@ export function ProtocolRunSetup({ ? flexDeckHardwareDescription : ot2DeckHardwareDescription, rightElProps: { - stepKey: MODULE_SETUP_KEY, + stepKey: MODULE_SETUP_STEP_KEY, complete: calibrationStatusModules.complete && !isMissingModule && @@ -272,84 +212,89 @@ export function ProtocolRunSetup({ incompleteElement: null, }, }, - [LPC_KEY]: { + [LPC_STEP_KEY]: { stepInternals: ( { - setLpcComplete(confirmed) + dispatch( + updateRunSetupStepsComplete(runId, { [LPC_STEP_KEY]: confirmed }) + ) if (confirmed) { - setExpandedStepKey(LABWARE_SETUP_KEY) - setMissingSteps( - missingSteps.filter(step => step !== 'applied_labware_offsets') - ) + setExpandedStepKey(LABWARE_SETUP_STEP_KEY) } }} - offsetsConfirmed={lpcComplete} + offsetsConfirmed={!missingSteps.includes(LPC_STEP_KEY)} /> ), description: t('labware_position_check_step_description'), rightElProps: { - stepKey: LPC_KEY, - complete: lpcComplete, + stepKey: LPC_STEP_KEY, + complete: !missingSteps.includes(LPC_STEP_KEY), completeText: t('offsets_ready'), incompleteText: null, incompleteElement: , }, }, - [LABWARE_SETUP_KEY]: { + [LABWARE_SETUP_STEP_KEY]: { stepInternals: ( { - setLabwareSetupComplete(confirmed) + dispatch( + updateRunSetupStepsComplete(runId, { + [LABWARE_SETUP_STEP_KEY]: confirmed, + }) + ) if (confirmed) { - setMissingSteps( - missingSteps.filter(step => step !== 'labware_placement') - ) const nextStep = - targetStepKeyInOrder.findIndex(v => v === LABWARE_SETUP_KEY) === - targetStepKeyInOrder.length - 1 + orderedApplicableSteps.findIndex( + v => v === LABWARE_SETUP_STEP_KEY + ) === + orderedApplicableSteps.length - 1 ? null - : LIQUID_SETUP_KEY + : LIQUID_SETUP_STEP_KEY setExpandedStepKey(nextStep) } }} /> ), - description: t(`${LABWARE_SETUP_KEY}_description`), + description: t(`${LABWARE_SETUP_STEP_KEY}_description`), rightElProps: { - stepKey: LABWARE_SETUP_KEY, - complete: labwareSetupComplete, + stepKey: LABWARE_SETUP_STEP_KEY, + complete: !missingSteps.includes(LABWARE_SETUP_STEP_KEY), completeText: t('placements_ready'), incompleteText: null, incompleteElement: null, }, }, - [LIQUID_SETUP_KEY]: { + [LIQUID_SETUP_STEP_KEY]: { stepInternals: ( { - setLiquidSetupComplete(confirmed) + dispatch( + updateRunSetupStepsComplete(runId, { + [LIQUID_SETUP_STEP_KEY]: confirmed, + }) + ) if (confirmed) { - setMissingSteps(missingSteps.filter(step => step !== 'liquids')) setExpandedStepKey(null) } }} /> ), description: hasLiquids - ? t(`${LIQUID_SETUP_KEY}_description`) + ? t(`${LIQUID_SETUP_STEP_KEY}_description`) : i18n.format(t('liquids_not_in_the_protocol'), 'capitalize'), rightElProps: { - stepKey: LIQUID_SETUP_KEY, - complete: liquidSetupComplete, + stepKey: LIQUID_SETUP_STEP_KEY, + complete: !missingSteps.includes(LIQUID_SETUP_STEP_KEY), completeText: t('liquids_ready'), incompleteText: null, incompleteElement: null, @@ -373,7 +318,7 @@ export function ProtocolRunSetup({ {t('protocol_analysis_failed')} ) : ( - stepsKeysInOrder.map((stepKey, index) => { + orderedSteps.map((stepKey, index) => { const setupStepTitle = t(`${stepKey}_title`) const showEmptySetupStep = (stepKey === 'liquid_setup_step' && !hasLiquids) || @@ -411,7 +356,7 @@ export function ProtocolRunSetup({ {StepDetailMap[stepKey].stepInternals} )} - {index !== stepsKeysInOrder.length - 1 ? ( + {index !== orderedSteps.length - 1 ? ( ) : null} @@ -431,7 +376,7 @@ export function ProtocolRunSetup({ interface NoHardwareRequiredStepCompletion { stepKey: Exclude< StepKey, - typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_KEY + typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_STEP_KEY > complete: boolean incompleteText: string | null @@ -440,7 +385,7 @@ interface NoHardwareRequiredStepCompletion { } interface HardwareRequiredStepCompletion { - stepKey: typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_KEY + stepKey: typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_STEP_KEY complete: boolean missingHardware: boolean incompleteText: string | null @@ -457,7 +402,7 @@ const stepRequiresHW = ( props: StepRightElementProps ): props is HardwareRequiredStepCompletion => props.stepKey === ROBOT_CALIBRATION_STEP_KEY || - props.stepKey === MODULE_SETUP_KEY + props.stepKey === MODULE_SETUP_STEP_KEY function StepRightElement(props: StepRightElementProps): JSX.Element | null { if (props.complete) { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx index 90745500149..5202419e290 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx @@ -23,7 +23,7 @@ import { RUN_STATUS_STOPPED } from '@opentrons/api-client' import { useIsFlex } from '/app/redux-resources/robots' import type { ProtocolCalibrationStatus } from '/app/redux/calibration/types' -import type { StepKey } from './ProtocolRunSetup' +import type { StepKey } from '/app/redux/protocol-runs' interface SetupRobotCalibrationProps { robotName: string diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 64aa0d094ae..84e7fb82e65 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -30,7 +30,9 @@ import { } from '/app/resources/runs' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { useRobot, useIsFlex } from '/app/redux-resources/robots' +import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import { SetupLabware } from '../SetupLabware' import { SetupRobotCalibration } from '../SetupRobotCalibration' @@ -38,7 +40,9 @@ import { SetupLiquids } from '../SetupLiquids' import { SetupModuleAndDeck } from '../SetupModuleAndDeck' import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' -import type { MissingSteps } from '../ProtocolRunSetup' +import * as ReduxRuns from '/app/redux/protocol-runs' + +import type { State } from '/app/redux/types' import type * as SharedData from '@opentrons/shared-data' @@ -56,9 +60,12 @@ vi.mock('/app/resources/runs/useUnmatchedModulesForProtocol') vi.mock('/app/resources/runs/useModuleCalibrationStatus') vi.mock('/app/resources/runs/useProtocolAnalysisErrors') vi.mock('/app/redux/config') +vi.mock('/app/redux/protocol-runs') +vi.mock('/app/resources/protocol-runs') vi.mock('/app/resources/deck_configuration/utils') vi.mock('/app/resources/deck_configuration/hooks') vi.mock('/app/redux-resources/robots') +vi.mock('/app/redux-resources/runs') vi.mock('/app/resources/analysis') vi.mock('@opentrons/shared-data', async importOriginal => { const actualSharedData = await importOriginal() @@ -74,20 +81,15 @@ vi.mock('@opentrons/shared-data', async importOriginal => { const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_PROTOCOL_LIQUID_KEY = { liquids: [] } -let mockMissingSteps: MissingSteps = [] -const mockSetMissingSteps = vi.fn((missingSteps: MissingSteps) => { - mockMissingSteps = missingSteps -}) const render = () => { - return renderWithProviders( + return renderWithProviders( , { + initialState: {} as State, i18nInstance: i18n, } )[0] @@ -95,7 +97,6 @@ const render = () => { describe('ProtocolRunSetup', () => { beforeEach(() => { - mockMissingSteps = [] when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(false) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) @@ -103,6 +104,9 @@ describe('ProtocolRunSetup', () => { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY, } as any) + when(vi.mocked(getMissingSetupSteps)) + .calledWith(expect.any(Object), RUN_ID) + .thenReturn([]) when(vi.mocked(useProtocolAnalysisErrors)).calledWith(RUN_ID).thenReturn({ analysisErrors: null, }) @@ -112,6 +116,27 @@ describe('ProtocolRunSetup', () => { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY, } as unknown) as SharedData.ProtocolAnalysisOutput) + when(vi.mocked(useRequiredSetupStepsInOrder)) + .calledWith({ + runId: RUN_ID, + protocolAnalysis: expect.any(Object), + }) + .thenReturn({ + orderedSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.MODULE_SETUP_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ReduxRuns.LIQUID_SETUP_STEP_KEY, + ], + orderedApplicableSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.MODULE_SETUP_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ReduxRuns.LIQUID_SETUP_STEP_KEY, + ], + }) vi.mocked(parseAllRequiredModuleModels).mockReturnValue([]) vi.mocked(parseLiquidsInLoadOrder).mockReturnValue([]) when(vi.mocked(useRobot)) @@ -179,6 +204,20 @@ describe('ProtocolRunSetup', () => { when(vi.mocked(useStoredProtocolAnalysis)) .calledWith(RUN_ID) .thenReturn(null) + when(vi.mocked(useRequiredSetupStepsInOrder)) + .calledWith({ runId: RUN_ID, protocolAnalysis: null }) + .thenReturn({ + orderedSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ], + orderedApplicableSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ], + }) render() screen.getByText('Loading data...') }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx index a00335f6475..d73d402585d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx @@ -8,7 +8,7 @@ import { RUN_STATUS_RUNNING, RUN_STATUS_STOP_REQUESTED, } from '@opentrons/api-client' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getLoadedLabwareDefinitionsByUri } from '@opentrons/shared-data' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -33,7 +33,13 @@ vi.mock('/app/redux/config') vi.mock('../RecoverySplash') vi.mock('/app/redux-resources/analytics') vi.mock('@opentrons/react-api-client') -vi.mock('/app/local-resources/labware') +vi.mock('@opentrons/shared-data', async () => { + const actual = await vi.importActual('@opentrons/shared-data') + return { + ...actual, + getLoadedLabwareDefinitionsByUri: vi.fn(), + } +}) vi.mock('react-redux', async () => { const actual = await vi.importActual('react-redux') return { @@ -45,7 +51,6 @@ vi.mock('react-redux', async () => { describe('useErrorRecoveryFlows', () => { beforeEach(() => { vi.mocked(useCurrentlyRecoveringFrom).mockReturnValue('mockCommand' as any) - vi.mocked(getLabwareDefinitionsFromCommands).mockReturnValue([]) }) it('should have initial state of isERActive as false', () => { @@ -143,7 +148,7 @@ describe('ErrorRecoveryFlows', () => { runStatus: RUN_STATUS_AWAITING_RECOVERY, failedCommandByRunRecord: mockFailedCommand, runId: 'MOCK_RUN_ID', - protocolAnalysis: {} as any, + protocolAnalysis: null, } vi.mocked(ErrorRecoveryWizard).mockReturnValue(
MOCK WIZARD
) vi.mocked(RecoverySplash).mockReturnValue(
MOCK RUN PAUSED SPLASH
) @@ -168,6 +173,7 @@ describe('ErrorRecoveryFlows', () => { intent: 'recovering', showTakeover: false, }) + vi.mocked(getLoadedLabwareDefinitionsByUri).mockReturnValue({}) }) it('renders the wizard when showERWizard is true', () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useFailedLabwareUtils.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useFailedLabwareUtils.test.tsx index ab12a0e7280..b0716af5c8a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useFailedLabwareUtils.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useFailedLabwareUtils.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { screen } from '@testing-library/react' +import { screen, renderHook } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -168,8 +168,8 @@ const TestWrapper = (props: GetRelevantLwLocationsParams) => { const displayLocation = useRelevantFailedLwLocations(props) return ( <> -
{`Current Loc: ${displayLocation.currentLoc}`}
-
{`New Loc: ${displayLocation.newLoc}`}
+
{`Current Loc: ${displayLocation.displayNameCurrentLoc}`}
+
{`New Loc: ${displayLocation.displayNameNewLoc}`}
) } @@ -181,7 +181,7 @@ const render = (props: ComponentProps) => { } describe('useRelevantFailedLwLocations', () => { - const mockProtocolAnalysis = {} as any + const mockRunRecord = { data: { modules: [], labware: [] } } as any const mockFailedLabware = { location: { slotName: 'D1' }, } as any @@ -194,14 +194,25 @@ describe('useRelevantFailedLwLocations', () => { render({ failedLabware: mockFailedLabware, failedCommandByRunRecord: mockFailedCommand, - protocolAnalysis: mockProtocolAnalysis, + runRecord: mockRunRecord, }) screen.getByText('Current Loc: Slot D1') screen.getByText('New Loc: null') + + const { result } = renderHook(() => + useRelevantFailedLwLocations({ + failedLabware: mockFailedLabware, + failedCommandByRunRecord: mockFailedCommand, + runRecord: mockRunRecord, + }) + ) + + expect(result.current.currentLoc).toStrictEqual({ slotName: 'D1' }) + expect(result.current.newLoc).toBeNull() }) - it('should return current and new location for moveLabware commands', () => { + it('should return current and new locations for moveLabware commands', () => { const mockFailedCommand = { commandType: 'moveLabware', params: { @@ -212,10 +223,21 @@ describe('useRelevantFailedLwLocations', () => { render({ failedLabware: mockFailedLabware, failedCommandByRunRecord: mockFailedCommand, - protocolAnalysis: mockProtocolAnalysis, + runRecord: mockRunRecord, }) screen.getByText('Current Loc: Slot D1') screen.getByText('New Loc: Slot C2') + + const { result } = renderHook(() => + useRelevantFailedLwLocations({ + failedLabware: mockFailedLabware, + failedCommandByRunRecord: mockFailedCommand, + runRecord: mockRunRecord, + }) + ) + + expect(result.current.currentLoc).toStrictEqual({ slotName: 'D1' }) + expect(result.current.newLoc).toStrictEqual({ slotName: 'C2' }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts index 95dac5abdb7..06453d06d08 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { getDeckDefFromRobotType, - getLoadedLabwareDefinitionsByUri, getFixedTrashLabwareDefinition, getModuleDef2, getPositionFromSlotId, @@ -11,6 +10,11 @@ import { THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' +import { + getRunLabwareRenderInfo, + getRunModuleRenderInfo, +} from '/app/organisms/InterventionModal/utils' + import type { Run } from '@opentrons/api-client' import type { DeckDefinition, @@ -22,14 +26,21 @@ import type { LoadedLabware, RobotType, LabwareDefinitionsByUri, + LoadedModule, } from '@opentrons/shared-data' import type { ErrorRecoveryFlowsProps } from '..' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' +import type { + RunLabwareInfo, + RunModuleInfo, +} from '/app/organisms/InterventionModal/utils' +import type { ERUtilsProps } from './useERUtils' interface UseDeckMapUtilsProps { runId: ErrorRecoveryFlowsProps['runId'] protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis'] failedLabwareUtils: UseFailedLabwareUtilsResult + labwareDefinitionsByUri: ERUtilsProps['labwareDefinitionsByUri'] runRecord?: Run } @@ -37,6 +48,11 @@ export interface UseDeckMapUtilsResult { deckConfig: CutoutConfigProtocolSpec[] modulesOnDeck: RunCurrentModulesOnDeck[] labwareOnDeck: RunCurrentLabwareOnDeck[] + loadedLabware: LoadedLabware[] + loadedModules: LoadedModule[] + movedLabwareDef: LabwareDefinition2 | null + moduleRenderInfo: RunModuleInfo[] + labwareRenderInfo: RunLabwareInfo[] highlightLabwareEventuallyIn: string[] kind: 'intervention' robotType: RobotType @@ -47,19 +63,12 @@ export function useDeckMapUtils({ runRecord, runId, failedLabwareUtils, + labwareDefinitionsByUri, }: UseDeckMapUtilsProps): UseDeckMapUtilsResult { const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis) const deckDef = getDeckDefFromRobotType(robotType) - const labwareDefinitionsByUri = useMemo( - () => - protocolAnalysis != null - ? getLoadedLabwareDefinitionsByUri(protocolAnalysis?.commands) - : null, - [protocolAnalysis] - ) - const currentModulesInfo = useMemo( () => getRunCurrentModulesInfo({ @@ -93,6 +102,35 @@ export function useDeckMapUtils({ [runId, protocolAnalysis, runRecord, deckDef, failedLabwareUtils] ) + const movedLabwareDef = + labwareDefinitionsByUri != null && failedLabwareUtils.failedLabware != null + ? labwareDefinitionsByUri[failedLabwareUtils.failedLabware.definitionUri] + : null + + const moduleRenderInfo = useMemo( + () => + runRecord != null && labwareDefinitionsByUri != null + ? getRunModuleRenderInfo( + runRecord.data, + deckDef, + labwareDefinitionsByUri + ) + : [], + [deckDef, labwareDefinitionsByUri, runRecord] + ) + + const labwareRenderInfo = useMemo( + () => + runRecord != null && labwareDefinitionsByUri != null + ? getRunLabwareRenderInfo( + runRecord.data, + labwareDefinitionsByUri, + deckDef + ) + : [], + [deckDef, labwareDefinitionsByUri, runRecord] + ) + return { deckConfig, modulesOnDeck: runCurrentModules.map( @@ -112,6 +150,11 @@ export function useDeckMapUtils({ .filter(maybeSlot => maybeSlot != null) as string[], kind: 'intervention', robotType, + loadedModules: runRecord?.data.modules ?? [], + loadedLabware: runRecord?.data.labware ?? [], + movedLabwareDef, + moduleRenderInfo, + labwareRenderInfo, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index ee8c4de6b83..57691a30e55 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -20,7 +20,11 @@ import { useShowDoorInfo } from './useShowDoorInfo' import { useCleanupRecoveryState } from './useCleanupRecoveryState' import { useFailedPipetteUtils } from './useFailedPipetteUtils' -import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' +import type { + LabwareDefinition2, + LabwareDefinitionsByUri, + RobotType, +} from '@opentrons/shared-data' import type { IRecoveryMap, RouteStep, RecoveryRoute } from '../types' import type { ErrorRecoveryFlowsProps } from '..' import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' @@ -48,6 +52,7 @@ export type ERUtilsProps = Omit & { failedCommand: ReturnType showTakeover: boolean allRunDefs: LabwareDefinition2[] + labwareDefinitionsByUri: LabwareDefinitionsByUri | null } export interface ERUtilsResults { @@ -82,6 +87,7 @@ export function useERUtils({ runStatus, showTakeover, allRunDefs, + labwareDefinitionsByUri, }: ERUtilsProps): ERUtilsResults { const { data: attachedInstruments } = useInstrumentsQuery() const { data: runRecord } = useNotifyRunQuery(runId) @@ -168,6 +174,7 @@ export function useERUtils({ runRecord, protocolAnalysis, failedLabwareUtils, + labwareDefinitionsByUri, }) const recoveryActionMutationUtils = useRecoveryActionMutation( diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index 239cb6f9e3d..d108bfb7d0a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -26,6 +26,7 @@ import type { DispenseRunTimeCommand, LiquidProbeRunTimeCommand, MoveLabwareRunTimeCommand, + LabwareLocation, } from '@opentrons/shared-data' import type { LabwareDisplayLocationSlotOnly } from '/app/local-resources/labware' import type { ErrorRecoveryFlowsProps } from '..' @@ -40,8 +41,10 @@ interface UseFailedLabwareUtilsProps { } interface RelevantFailedLabwareLocations { - currentLoc: string - newLoc: string | null + displayNameCurrentLoc: string + displayNameNewLoc: string | null + currentLoc: LabwareLocation | null + newLoc: LabwareLocation | null } export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & { @@ -53,6 +56,7 @@ export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & { relevantWellName: string | null /* The user-content nickname of the failed labware, if any */ failedLabwareNickname: string | null + /* Details relating to the labware location. */ failedLabwareLocations: RelevantFailedLabwareLocations } @@ -103,7 +107,7 @@ export function useFailedLabwareUtils({ const failedLabwareLocations = useRelevantFailedLwLocations({ failedLabware, failedCommandByRunRecord, - protocolAnalysis, + runRecord, }) return { @@ -336,7 +340,7 @@ export function getRelevantWellName( export type GetRelevantLwLocationsParams = Pick< UseFailedLabwareUtilsProps, - 'protocolAnalysis' | 'failedCommandByRunRecord' + 'runRecord' | 'failedCommandByRunRecord' > & { failedLabware: UseFailedLabwareUtilsResult['failedLabware'] } @@ -344,7 +348,7 @@ export type GetRelevantLwLocationsParams = Pick< export function useRelevantFailedLwLocations({ failedLabware, failedCommandByRunRecord, - protocolAnalysis, + runRecord, }: GetRelevantLwLocationsParams): RelevantFailedLabwareLocations { const { t } = useTranslation('protocol_command_text') @@ -352,33 +356,43 @@ export function useRelevantFailedLwLocations({ LabwareDisplayLocationSlotOnly, 'location' > = { - loadedLabwares: protocolAnalysis?.labware ?? [], - loadedModules: protocolAnalysis?.modules ?? [], + loadedLabwares: runRecord?.data?.labware ?? [], + loadedModules: runRecord?.data?.modules ?? [], robotType: FLEX_ROBOT_TYPE, t, detailLevel: 'slot-only', isOnDevice: false, // Always return the "slot XYZ" copy, which is the desktop copy. } - const currentLocation = getLabwareDisplayLocation({ + const displayNameCurrentLoc = getLabwareDisplayLocation({ ...BASE_DISPLAY_PARAMS, location: failedLabware?.location ?? null, }) - const getNewLocation = (): string | null => { + const getNewLocation = (): Pick< + RelevantFailedLabwareLocations, + 'displayNameNewLoc' | 'newLoc' + > => { switch (failedCommandByRunRecord?.commandType) { case 'moveLabware': - return getLabwareDisplayLocation({ - ...BASE_DISPLAY_PARAMS, - location: failedCommandByRunRecord.params.newLocation, - }) + return { + displayNameNewLoc: getLabwareDisplayLocation({ + ...BASE_DISPLAY_PARAMS, + location: failedCommandByRunRecord.params.newLocation, + }), + newLoc: failedCommandByRunRecord.params.newLocation, + } default: - return null + return { + displayNameNewLoc: null, + newLoc: null, + } } } return { - currentLoc: currentLocation, - newLoc: getNewLocation(), + displayNameCurrentLoc, + currentLoc: failedLabware?.location ?? null, + ...getNewLocation(), } } diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index 2bd26beb747..124c4fea65f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -13,11 +13,13 @@ import { RUN_STATUS_STOP_REQUESTED, RUN_STATUS_SUCCEEDED, } from '@opentrons/api-client' -import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { + getLoadedLabwareDefinitionsByUri, + OT2_ROBOT_TYPE, +} from '@opentrons/shared-data' import { useHost } from '@opentrons/react-api-client' import { getIsOnDevice } from '/app/redux/config' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { ErrorRecoveryWizard, useERWizard } from './ErrorRecoveryWizard' import { RecoverySplash, useRecoverySplash } from './RecoverySplash' import { RecoveryTakeover } from './RecoveryTakeover' @@ -127,13 +129,19 @@ export function ErrorRecoveryFlows( const robotName = useHost()?.robotName ?? 'robot' const isValidRobotSideAnalysis = protocolAnalysis != null - const allRunDefs = useMemo( + + // TODO(jh, 10-22-24): EXEC-769. + const labwareDefinitionsByUri = useMemo( () => protocolAnalysis != null - ? getLabwareDefinitionsFromCommands(protocolAnalysis.commands) - : [], + ? getLoadedLabwareDefinitionsByUri(protocolAnalysis?.commands) + : null, [isValidRobotSideAnalysis] ) + const allRunDefs = + labwareDefinitionsByUri != null + ? Object.values(labwareDefinitionsByUri) + : [] const { showTakeover, @@ -151,6 +159,7 @@ export function ErrorRecoveryFlows( showTakeover, failedCommand: failedCommandBySource, allRunDefs, + labwareDefinitionsByUri, }) const renderWizard = diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/GripperReleaseLabware.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/GripperReleaseLabware.tsx index f57357f6451..3e0c24756d2 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/GripperReleaseLabware.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/GripperReleaseLabware.tsx @@ -7,6 +7,7 @@ import { RESPONSIVENESS, Flex, StyledText, + JUSTIFY_CENTER, } from '@opentrons/components' import { TwoColumn } from '/app/molecules/InterventionModal' @@ -15,6 +16,8 @@ import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' +import gripperReleaseAnimation from '/app/assets/videos/error-recovery/Gripper_Release.webm' + import type { JSX } from 'react' import type { RecoveryContentProps } from '../types' @@ -51,7 +54,20 @@ export function GripperReleaseLabware({ heading={t('labware_released_from_current_height')} /> -
ANIMATION GOES HERE
+ + + ['infoProps']['newLocationProps'] => - newLoc != null ? { deckLabel: newLoc.toUpperCase() } : undefined + displayNameNewLoc != null + ? { deckLabel: displayNameNewLoc.toUpperCase() } + : undefined return ( { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: { + const { newLoc, currentLoc } = failedLabwareUtils.failedLabwareLocations + const { + movedLabwareDef, + moduleRenderInfo, + labwareRenderInfo, + ...restUtils + } = deckMapUtils + + const failedLwId = failedLabware?.id ?? '' + + const isValidDeck = + currentLoc != null && newLoc != null && movedLabwareDef != null + + return isValidDeck ? ( + + {moduleRenderInfo.map( + ({ + x, + y, + moduleId, + moduleDef, + nestedLabwareDef, + nestedLabwareId, + }) => ( + + {nestedLabwareDef != null && + nestedLabwareId !== failedLwId ? ( + + ) : null} + + ) + )} + {labwareRenderInfo + .filter(l => l.labwareId !== failedLwId) + .map(({ x, y, labwareDef, labwareId }) => ( + + {labwareDef != null && labwareId !== failedLwId ? ( + + ) : null} + + ))} + + } + /> + ) : ( + + ) + } + default: + return + } + } + return ( @@ -109,9 +187,7 @@ export function TwoColLwInfoAndDeck( type={buildType()} bannerText={buildBannerText()} /> - - - + {buildDeckView()} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx index 9eff4a09ba4..3bdd9f97819 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/GripperReleaseLabware.test.tsx @@ -9,6 +9,10 @@ import { clickButtonLabeled } from '/app/organisms/ErrorRecoveryFlows/__tests__/ import type { Mock } from 'vitest' +vi.mock('/app/assets/videos/error-recovery/Gripper_Release.webm', () => ({ + default: 'mocked-animation-path.webm', +})) + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -48,4 +52,14 @@ describe('GripperReleaseLabware', () => { expect(mockHandleMotionRouting).toHaveBeenCalled() }) + + it('renders gripper animation', () => { + render(props) + + screen.getByRole('presentation', { hidden: true }) + expect(screen.getByTestId('gripper-animation')).toHaveAttribute( + 'src', + 'mocked-animation-path.webm' + ) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx index e2e6c268ef8..f38e1e06922 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx @@ -27,8 +27,8 @@ describe('LeftColumnLabwareInfo', () => { failedLabwareName: 'MOCK_LW_NAME', failedLabwareNickname: 'MOCK_LW_NICKNAME', failedLabwareLocations: { - currentLoc: 'slot A1', - newLoc: 'slot B2', + displayNameCurrentLoc: 'slot A1', + displayNameNewLoc: 'slot B2', }, } as any, type: 'location', @@ -76,7 +76,7 @@ describe('LeftColumnLabwareInfo', () => { }) it('does not include newLocationProps when newLoc is not provided', () => { - props.failedLabwareUtils.failedLabwareLocations.newLoc = null + props.failedLabwareUtils.failedLabwareLocations.displayNameNewLoc = null render(props) expect(vi.mocked(InterventionContent)).toHaveBeenCalledWith( @@ -91,9 +91,12 @@ describe('LeftColumnLabwareInfo', () => { it('converts location labels to uppercase', () => { props.failedLabwareUtils.failedLabwareLocations = { - currentLoc: 'slot A1', - newLoc: 'slot B2', + displayNameCurrentLoc: 'slot A1', + displayNameNewLoc: 'slot B2', + newLoc: {} as any, + currentLoc: {} as any, } + render(props) expect(vi.mocked(InterventionContent)).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 9a8fc10f5d6..08db6269c4d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -53,7 +53,10 @@ describe('SelectTips', () => { failedLabwareUtils: { selectedTipLocations: { A1: null }, areTipsSelected: true, - failedLabwareLocations: { newLoc: null, currentLoc: 'A1' }, + failedLabwareLocations: { + displayNameNewLoc: null, + displayNameCurrentLoc: 'A1', + }, } as any, } @@ -161,7 +164,10 @@ describe('SelectTips', () => { failedLabwareUtils: { selectedTipLocations: null, areTipsSelected: false, - failedLabwareLocations: { newLoc: null, currentLoc: '' }, + failedLabwareLocations: { + displayNameNewLoc: null, + displayNameCurrentLoc: '', + }, } as any, } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx index f2206c8f010..2f24fc0f3bb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/TwoColLwInfoAndDeck.test.tsx @@ -1,4 +1,7 @@ import { describe, it, vi, expect, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' + +import { MoveLabwareOnDeck } from '@opentrons/components' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -11,6 +14,13 @@ import { getSlotNameAndLwLocFrom } from '../../hooks/useDeckMapUtils' import type * as React from 'react' import type { Mock } from 'vitest' +vi.mock('@opentrons/components', async () => { + const actual = await vi.importActual('@opentrons/components') + return { + ...actual, + MoveLabwareOnDeck: vi.fn(), + } +}) vi.mock('../LeftColumnLabwareInfo') vi.mock('../../hooks/useDeckMapUtils') @@ -39,11 +49,17 @@ describe('TwoColLwInfoAndDeck', () => { failedLabwareUtils: { relevantWellName: 'A1', failedLabware: { location: 'C1' }, + failedLabwareLocations: { newLoc: {}, currentLoc: {} }, + }, + deckMapUtils: { + movedLabwareDef: {}, + moduleRenderInfo: [], + labwareRenderInfo: [], }, - deckMapUtils: {}, currentRecoveryOptionUtils: { selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, }, + isOnDevice: true, } as any vi.mocked(LeftColumnLabwareInfo).mockReturnValue( @@ -131,4 +147,34 @@ describe('TwoColLwInfoAndDeck', () => { expect.anything() ) }) + + it(`renders a move labware on deck view if the selected recovery option is ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE} and props are valid`, () => { + vi.mocked(MoveLabwareOnDeck).mockReturnValue( +
MOCK_MOVE_LW_ON_DECK
+ ) + + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE + render(props) + + screen.getByText('MOCK_MOVE_LW_ON_DECK') + }) + + it(`does not render a move labware on deck view if the selected recovery option is ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE} and props are invalid`, () => { + vi.mocked(MoveLabwareOnDeck).mockReturnValue( +
MOCK_MOVE_LW_ON_DECK
+ ) + + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE + props.deckMapUtils = { + movedLabwareDef: null, + moduleRenderInfo: null, + labwareRenderInfo: null, + } as any + + render(props) + + expect(screen.queryByText('MOCK_MOVE_LW_ON_DECK')).not.toBeInTheDocument() + }) }) diff --git a/app/src/organisms/ODD/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts b/app/src/organisms/ODD/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts index 29e0847bf02..38d62455854 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts +++ b/app/src/organisms/ODD/QuickTransferFlow/utils/generateCompatibleLabwareForPipette.ts @@ -14,7 +14,8 @@ export function generateCompatibleLabwareForPipette( (acc, definition) => { if ( definition.allowedRoles != null && - definition.allowedRoles.includes('adapter') + (definition.allowedRoles.includes('adapter') || + definition.allowedRoles.includes('lid')) ) { return acc } else if (pipetteSpecs.channels === 1) { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx index a290e689809..bc738d0caf3 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx @@ -24,7 +24,7 @@ describe('UnskippableModal', () => { render(props) screen.getByText('This is a critical step that should not be skipped') screen.getByText( - 'You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.' + 'You must detach the mounting plate and reattach the z-axis carriage before using other pipettes. We do not recommend exiting this process before completion.' ) fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(props.goBack).toHaveBeenCalled() @@ -39,7 +39,7 @@ describe('UnskippableModal', () => { render(props) screen.getByText('This is a critical step that should not be skipped') screen.getByText( - 'You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.' + 'You must detach the mounting plate and reattach the z-axis carriage before using other pipettes. We do not recommend exiting this process before completion.' ) screen.getByText('Exit') screen.getByText('Go back') diff --git a/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx index 7ea4ceae7c6..4abb609c4fc 100644 --- a/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx @@ -25,10 +25,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client' import { useSyncRobotClock } from '/app/organisms/Desktop/Devices/hooks' import { ProtocolRunHeader } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader' import { RunPreview } from '/app/organisms/Desktop/Devices/RunPreview' -import { - ProtocolRunSetup, - initialMissingSteps, -} from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup' +import { ProtocolRunSetup } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup' import { BackToTopButton } from '/app/organisms/Desktop/Devices/ProtocolRun/BackToTopButton' import { ProtocolRunModuleControls } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls' import { ProtocolRunRuntimeParameters } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunRunTimeParameters' @@ -187,10 +184,6 @@ function PageContents(props: PageContentsProps): JSX.Element { } }, [jumpedIndex]) - const [missingSteps, setMissingSteps] = useState< - ReturnType - >(initialMissingSteps()) - const makeHandleScrollToStep = (i: number) => () => { listRef.current?.scrollToIndex(i, true, -1 * JUMP_OFFSET_FROM_TOP_PX) } @@ -210,8 +203,6 @@ function PageContents(props: PageContentsProps): JSX.Element { protocolRunHeaderRef={protocolRunHeaderRef} robotName={robotName} runId={runId} - setMissingSteps={setMissingSteps} - missingSteps={missingSteps} /> ), backToTop: ( @@ -269,7 +260,6 @@ function PageContents(props: PageContentsProps): JSX.Element { robotName={robotName} runId={runId} makeHandleJumpToStep={makeHandleJumpToStep} - missingSetupSteps={missingSteps} /> { + const orderedSteps = + protocolAnalysis == null ? NO_ANALYSIS_STEPS_IN_ORDER : ALL_STEPS_IN_ORDER + + const orderedApplicableSteps = + protocolAnalysis == null + ? NO_ANALYSIS_STEPS_IN_ORDER + : ALL_STEPS_IN_ORDER.filter((stepKey: StepKey) => { + if (protocolAnalysis.modules.length === 0) { + return stepKey !== MODULE_SETUP_STEP_KEY + } + + if (protocolAnalysis.liquids.length === 0) { + return stepKey !== LIQUID_SETUP_STEP_KEY + } + return true + }) + return { orderedSteps: orderedSteps as StepKey[], orderedApplicableSteps } +} + +const keyFor = ( + analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null + // @ts-expect-error(sf, 2024-10-23): purposeful weak object typing +): string | null => analysis?.id ?? analysis?.metadata?.id ?? null + +export function useRequiredSetupStepsInOrder({ + runId, + protocolAnalysis, +}: UseRequiredSetupStepsInOrderProps): UseRequiredSetupStepsInOrderReturn { + const dispatch = useDispatch() + const requiredSteps = useSelector(state => + getSetupStepsRequired(state, runId) + ) + + useEffect(() => { + const applicable = keysInOrder(protocolAnalysis) + dispatch( + updateRunSetupStepsRequired(runId, { + ...ALL_STEPS_IN_ORDER.reduce< + UpdateRunSetupStepsRequiredAction['payload']['required'] + >( + (acc, thiskey) => ({ + ...acc, + [thiskey]: applicable.orderedApplicableSteps.includes(thiskey), + }), + {} + ), + }) + ) + }, [runId, keyFor(protocolAnalysis), dispatch]) + return protocolAnalysis == null + ? { + orderedSteps: NO_ANALYSIS_STEPS_IN_ORDER, + orderedApplicableSteps: NO_ANALYSIS_STEPS_IN_ORDER, + } + : { + orderedSteps: ALL_STEPS_IN_ORDER, + orderedApplicableSteps: ALL_STEPS_IN_ORDER.filter( + step => (requiredSteps as Required> | null)?.[step] + ), + } +} diff --git a/app/src/redux-resources/runs/index.ts b/app/src/redux-resources/runs/index.ts new file mode 100644 index 00000000000..fc78d35129c --- /dev/null +++ b/app/src/redux-resources/runs/index.ts @@ -0,0 +1 @@ +export * from './hooks' diff --git a/app/src/redux/protocol-runs/__tests__/reducer.test.ts b/app/src/redux/protocol-runs/__tests__/reducer.test.ts new file mode 100644 index 00000000000..e10ce306f7d --- /dev/null +++ b/app/src/redux/protocol-runs/__tests__/reducer.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest' + +import { protocolRunReducer } from '../reducer' +import { + updateRunSetupStepsComplete, + updateRunSetupStepsRequired, +} from '../actions' +import * as Constants from '../constants' + +describe('protocol runs reducer', () => { + const INITIAL = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.MODULE_SETUP_STEP_KEY]: { required: true, complete: false }, + [Constants.LPC_STEP_KEY]: { required: true, complete: false }, + [Constants.LABWARE_SETUP_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.LIQUID_SETUP_STEP_KEY]: { required: true, complete: false }, + } + it('establishes an empty state if you tell it one', () => { + const nextState = protocolRunReducer( + undefined, + updateRunSetupStepsComplete('some-run-id', {}) + ) + expect(nextState['some-run-id']?.setup).toEqual(INITIAL) + }) + it('updates complete based on an action', () => { + const nextState = protocolRunReducer( + { + 'some-run-id': { + setup: { + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + complete: true, + required: true, + }, + }, + }, + }, + updateRunSetupStepsComplete('some-run-id', { + [Constants.LPC_STEP_KEY]: true, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: true, + complete: true, + }, + [Constants.LPC_STEP_KEY]: { required: true, complete: true }, + }) + }) + it('updates required based on an action', () => { + const nextState = protocolRunReducer( + { + 'some-run-id': { + setup: INITIAL, + }, + }, + updateRunSetupStepsRequired('some-run-id', { + [Constants.LIQUID_SETUP_STEP_KEY]: false, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: false, + complete: false, + }, + }) + }) +}) diff --git a/app/src/redux/protocol-runs/actions.ts b/app/src/redux/protocol-runs/actions.ts new file mode 100644 index 00000000000..378ee297ed2 --- /dev/null +++ b/app/src/redux/protocol-runs/actions.ts @@ -0,0 +1,18 @@ +import * as Constants from './constants' +import type * as Types from './types' + +export const updateRunSetupStepsComplete = ( + runId: string, + complete: Types.UpdateRunSetupStepsCompleteAction['payload']['complete'] +): Types.UpdateRunSetupStepsCompleteAction => ({ + type: Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE, + payload: { runId, complete }, +}) + +export const updateRunSetupStepsRequired = ( + runId: string, + required: Types.UpdateRunSetupStepsRequiredAction['payload']['required'] +): Types.UpdateRunSetupStepsRequiredAction => ({ + type: Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED, + payload: { runId, required }, +}) diff --git a/app/src/redux/protocol-runs/constants.ts b/app/src/redux/protocol-runs/constants.ts new file mode 100644 index 00000000000..04f28f760d3 --- /dev/null +++ b/app/src/redux/protocol-runs/constants.ts @@ -0,0 +1,18 @@ +export const ROBOT_CALIBRATION_STEP_KEY: 'robot_calibration_step' = + 'robot_calibration_step' +export const MODULE_SETUP_STEP_KEY: 'module_setup_step' = 'module_setup_step' +export const LPC_STEP_KEY: 'labware_position_check_step' = + 'labware_position_check_step' +export const LABWARE_SETUP_STEP_KEY: 'labware_setup_step' = 'labware_setup_step' +export const LIQUID_SETUP_STEP_KEY: 'liquid_setup_step' = 'liquid_setup_step' + +export const SETUP_STEP_KEYS = [ + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, +] as const + +export const UPDATE_RUN_SETUP_STEPS_COMPLETE = 'protocolRuns:UPDATE_RUN_SETUP_STEPS_COMPLETE' as const +export const UPDATE_RUN_SETUP_STEPS_REQUIRED = 'protocolRuns:UPDATE_RUN_SETUP_STEPS_REQUIRED' as const diff --git a/app/src/redux/protocol-runs/index.ts b/app/src/redux/protocol-runs/index.ts new file mode 100644 index 00000000000..9f709c0dbcb --- /dev/null +++ b/app/src/redux/protocol-runs/index.ts @@ -0,0 +1,7 @@ +// runs constants, actions, selectors, and types + +export * from './actions' +export * from './constants' +export * from './selectors' + +export type * from './types' diff --git a/app/src/redux/protocol-runs/reducer.ts b/app/src/redux/protocol-runs/reducer.ts new file mode 100644 index 00000000000..0b2d8378a67 --- /dev/null +++ b/app/src/redux/protocol-runs/reducer.ts @@ -0,0 +1,63 @@ +import * as Constants from './constants' + +import type { Reducer } from 'redux' +import type { Action } from '../types' + +import type { ProtocolRunState, RunSetupStatus } from './types' + +const INITIAL_STATE: ProtocolRunState = {} + +const INITIAL_SETUP_STEP_STATE = { complete: false, required: true } + +const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, +} + +export const protocolRunReducer: Reducer = ( + state = INITIAL_STATE, + action +) => { + switch (action.type) { + case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + complete: + action.payload.complete[step] ?? currentState[step].complete, + required: currentState[step].required, + }, + }), + state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE + ), + }, + } + } + case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + required: + action.payload.required[step] ?? currentState[step].required, + complete: currentState[step].complete, + }, + }), + state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE + ), + }, + } + } + } + return state +} diff --git a/app/src/redux/protocol-runs/selectors.ts b/app/src/redux/protocol-runs/selectors.ts new file mode 100644 index 00000000000..ca91c7a71ab --- /dev/null +++ b/app/src/redux/protocol-runs/selectors.ts @@ -0,0 +1,91 @@ +import type { State } from '../types' +import type * as Types from './types' + +export const getSetupStepComplete: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsComplete(state, runId)?.[step] ?? null + +export const getSetupStepsComplete: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.complete, + }), + {} + ) as Types.StepMap +} + +export const getSetupStepRequired: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsRequired(state, runId)?.[step] ?? null + +export const getSetupStepsRequired: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>>( + (acc, [step, state]) => ({ ...acc, [step]: state.required }), + {} + ) as Types.StepMap +} + +export const getSetupStepMissing: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsMissing(state, runId)?.[step] || null + +export const getSetupStepsMissing: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.required && !state.complete, + }), + {} + ) as Types.StepMap +} + +export const getMissingSetupSteps: ( + state: State, + runId: string +) => Types.StepKey[] = (state, runId) => { + const missingStepMap = getSetupStepsMissing(state, runId) + if (missingStepMap == null) return [] + const missingStepList = (Object.entries(missingStepMap) as Array< + [Types.StepKey, boolean] + >) + .map(([step, missing]) => (missing ? step : null)) + .filter(stepName => stepName != null) + return missingStepList as Types.StepKey[] +} diff --git a/app/src/redux/protocol-runs/types.ts b/app/src/redux/protocol-runs/types.ts new file mode 100644 index 00000000000..c14d556d495 --- /dev/null +++ b/app/src/redux/protocol-runs/types.ts @@ -0,0 +1,61 @@ +import type { + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + UPDATE_RUN_SETUP_STEPS_COMPLETE, + UPDATE_RUN_SETUP_STEPS_REQUIRED, +} from './constants' + +export type RobotCalibrationStepKey = typeof ROBOT_CALIBRATION_STEP_KEY +export type ModuleSetupStepKey = typeof MODULE_SETUP_STEP_KEY +export type LPCStepKey = typeof LPC_STEP_KEY +export type LabwareSetupStepKey = typeof LABWARE_SETUP_STEP_KEY +export type LiquidSetupStepKey = typeof LIQUID_SETUP_STEP_KEY + +export type StepKey = + | RobotCalibrationStepKey + | ModuleSetupStepKey + | LPCStepKey + | LabwareSetupStepKey + | LiquidSetupStepKey + +export interface StepState { + required: boolean + complete: boolean +} + +export type StepMap = { [Step in StepKey]: V } + +export type RunSetupStatus = { + [Step in StepKey]: StepState +} + +export interface PerRunUIState { + setup: RunSetupStatus +} + +export type ProtocolRunState = Partial<{ + readonly [runId: string]: PerRunUIState +}> + +export interface UpdateRunSetupStepsCompleteAction { + type: typeof UPDATE_RUN_SETUP_STEPS_COMPLETE + payload: { + runId: string + complete: Partial<{ [Step in StepKey]: boolean }> + } +} + +export interface UpdateRunSetupStepsRequiredAction { + type: typeof UPDATE_RUN_SETUP_STEPS_REQUIRED + payload: { + runId: string + required: Partial<{ [Step in StepKey]: boolean }> + } +} + +export type ProtocolRunAction = + | UpdateRunSetupStepsCompleteAction + | UpdateRunSetupStepsRequiredAction diff --git a/app/src/redux/reducer.ts b/app/src/redux/reducer.ts index 44831b0d70e..e21dbded781 100644 --- a/app/src/redux/reducer.ts +++ b/app/src/redux/reducer.ts @@ -48,6 +48,9 @@ import { calibrationReducer } from './calibration/reducer' // local protocol storage from file system state import { protocolStorageReducer } from './protocol-storage/reducer' +// local protocol run state +import { protocolRunReducer } from './protocol-runs/reducer' + import type { Reducer } from 'redux' import type { State, Action } from './types' @@ -68,4 +71,5 @@ export const rootReducer: Reducer = combineReducers({ sessions: sessionReducer, calibration: calibrationReducer, protocolStorage: protocolStorageReducer, + protocolRuns: protocolRunReducer, }) diff --git a/app/src/redux/types.ts b/app/src/redux/types.ts index 9ed69c3e71f..d3f502cdc40 100644 --- a/app/src/redux/types.ts +++ b/app/src/redux/types.ts @@ -37,6 +37,8 @@ import type { AlertsState, AlertsAction } from './alerts/types' import type { SessionState, SessionsAction } from './sessions/types' import type { AnalyticsTriggerAction } from './analytics/types' +import type { ProtocolRunState, ProtocolRunAction } from './protocol-runs/types' + export interface State { readonly robotApi: RobotApiState readonly robotAdmin: RobotAdminState @@ -54,6 +56,7 @@ export interface State { readonly sessions: SessionState readonly calibration: CalibrationState readonly protocolStorage: ProtocolStorageState + readonly protocolRuns: ProtocolRunState } export type Action = @@ -78,6 +81,7 @@ export type Action = | CalibrationAction | AnalyticsTriggerAction | AddCustomLabwareFromCreatorAction + | ProtocolRunAction export type GetState = () => State diff --git a/components/src/atoms/ListItem/ListItem.stories.tsx b/components/src/atoms/ListItem/ListItem.stories.tsx index 0738b583cde..dbe4739249d 100644 --- a/components/src/atoms/ListItem/ListItem.stories.tsx +++ b/components/src/atoms/ListItem/ListItem.stories.tsx @@ -50,7 +50,7 @@ export const ListItemDescriptorDefault: Story = { type: 'noActive', children: ( mock content} description={
mock description
} /> @@ -63,7 +63,7 @@ export const ListItemDescriptorMini: Story = { type: 'noActive', children: ( mock content} description={
mock description
} /> diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx index 51d9ca9e181..12b494ea894 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx @@ -2,43 +2,34 @@ import { Flex } from '../../../primitives' import { ALIGN_FLEX_START, DIRECTION_ROW, - FLEX_AUTO, JUSTIFY_SPACE_BETWEEN, } from '../../../styles' import { SPACING } from '../../../ui-style-constants' interface ListItemDescriptorProps { - type: 'default' | 'mini' + type: 'default' | 'large' description: JSX.Element | string content: JSX.Element | string + isInSlideout?: boolean } export const ListItemDescriptor = ( props: ListItemDescriptorProps ): JSX.Element => { - const { description, content, type } = props + const { description, content, type, isInSlideout = false } = props return ( - + {description} - - {content} - + {content} ) } diff --git a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx index 2f25b883fae..9cb34e3524f 100644 --- a/components/src/atoms/ListItem/__tests__/ListItem.test.tsx +++ b/components/src/atoms/ListItem/__tests__/ListItem.test.tsx @@ -26,7 +26,6 @@ describe('ListItem', () => { screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_error') expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.red35}`) - expect(listItem).toHaveStyle(`padding: 0`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) it('should render correct style - noActive', () => { @@ -35,7 +34,6 @@ describe('ListItem', () => { screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_noActive') expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.grey30}`) - expect(listItem).toHaveStyle(`padding: 0`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) it('should render correct style - success', () => { @@ -44,7 +42,6 @@ describe('ListItem', () => { screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_success') expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.green35}`) - expect(listItem).toHaveStyle(`padding: 0`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) it('should render correct style - warning', () => { @@ -53,7 +50,6 @@ describe('ListItem', () => { screen.getByText('mock listitem content') const listItem = screen.getByTestId('ListItem_warning') expect(listItem).toHaveStyle(`backgroundColor: ${COLORS.yellow35}`) - expect(listItem).toHaveStyle(`padding: 0`) expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius4}`) }) it('should call on click when pressed', () => { diff --git a/components/src/atoms/ListItem/index.tsx b/components/src/atoms/ListItem/index.tsx index 39367f935c1..cb61f0a4d3c 100644 --- a/components/src/atoms/ListItem/index.tsx +++ b/components/src/atoms/ListItem/index.tsx @@ -56,7 +56,6 @@ export function ListItem(props: ListItemProps): JSX.Element { background-color: ${listItemProps.backgroundColor}; width: 100%; height: ${FLEX_MAX_CONTENT}; - padding: 0; border-radius: ${BORDERS.borderRadius4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { diff --git a/components/src/atoms/Toast/index.tsx b/components/src/atoms/Toast/index.tsx index 04a202b69cf..e4a9af243ab 100644 --- a/components/src/atoms/Toast/index.tsx +++ b/components/src/atoms/Toast/index.tsx @@ -370,7 +370,7 @@ export function Toast(props: ToastProps): JSX.Element { ) : null} {message} - {linkText ? ( + {linkText != null ? ( { diff --git a/components/src/images/labware/measurement-guide/index.ts b/components/src/images/labware/measurement-guide/index.ts index b866627db1d..6b6c5e14fee 100644 --- a/components/src/images/labware/measurement-guide/index.ts +++ b/components/src/images/labware/measurement-guide/index.ts @@ -107,6 +107,10 @@ const FOOTPRINT_DIAGRAMS: Diagrams = { new URL(FOOTPRINT_IMAGE_RELATIVE_PATH, import.meta.url).href, new URL(DIMENSIONS_HEIGHT_PLATE_IMAGE_RELATIVE_PATH, import.meta.url).href, ], + lid: [ + new URL(FOOTPRINT_IMAGE_RELATIVE_PATH, import.meta.url).href, + new URL(DIMENSIONS_HEIGHT_PLATE_IMAGE_RELATIVE_PATH, import.meta.url).href, + ], } const ALUM_BLOCK_FOOTPRINTS: Diagrams = { diff --git a/components/src/modals/Modal.tsx b/components/src/modals/Modal.tsx index fc823243b5d..7be1ca06340 100644 --- a/components/src/modals/Modal.tsx +++ b/components/src/modals/Modal.tsx @@ -6,6 +6,7 @@ import { ModalHeader } from './ModalHeader' import { ModalShell } from './ModalShell' import type { IconProps } from '../icons' import type { StyleProps } from '../primitives' +import type { Position } from './ModalShell' type ModalType = 'info' | 'warning' | 'error' @@ -21,6 +22,8 @@ export interface ModalProps extends StyleProps { children?: React.ReactNode footer?: React.ReactNode zIndexOverlay?: number + showOverlay?: boolean + position?: Position } /** @@ -38,6 +41,8 @@ export const Modal = (props: ModalProps): JSX.Element => { titleElement1, titleElement2, zIndexOverlay, + position, + showOverlay, ...styleProps } = props @@ -72,9 +77,10 @@ export const Modal = (props: ModalProps): JSX.Element => { backgroundColor={COLORS.white} /> ) - return ( { @@ -61,7 +71,7 @@ export function ModalShell(props: ModalShellProps): JSX.Element { if (onOutsideClick != null) onOutsideClick(e) }} > - + ) } -const Overlay = styled.div<{ zIndex: string | number }>` +const Overlay = styled.div<{ zIndex: string | number; showOverlay: boolean }>` position: ${POSITION_ABSOLUTE}; left: 0; right: 0; top: 0; bottom: 0; z-index: ${({ zIndex }) => zIndex}; - background-color: ${COLORS.black90}${COLORS.opacity40HexCode}; + background-color: ${({ showOverlay }) => + showOverlay + ? `${COLORS.black90}${COLORS.opacity40HexCode}` + : COLORS.transparent}; cursor: ${CURSOR_DEFAULT}; ` -const ContentArea = styled.div<{ zIndex: string | number }>` +const ContentArea = styled.div<{ zIndex: string | number; position: Position }>` display: flex; position: ${POSITION_ABSOLUTE}; - align-items: ${ALIGN_CENTER}; - justify-content: ${JUSTIFY_CENTER}; + align-items: ${({ position }) => + position === 'center' ? ALIGN_CENTER : ALIGN_END}; + justify-content: ${({ position }) => + position === 'center' ? JUSTIFY_CENTER : JUSTIFY_END}; top: 0; right: 0; bottom: 0; diff --git a/components/src/organisms/EndUserAgreementFooter/__tests__/EndUserAgreementFooter.test.tsx b/components/src/organisms/EndUserAgreementFooter/__tests__/EndUserAgreementFooter.test.tsx new file mode 100644 index 00000000000..a13aacfe27f --- /dev/null +++ b/components/src/organisms/EndUserAgreementFooter/__tests__/EndUserAgreementFooter.test.tsx @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../testing/utils' +import { EndUserAgreementFooter } from '../index' + +const render = () => { + return renderWithProviders() +} + +describe('EndUserAgreementFooter', () => { + it('should render text and links', () => { + render() + screen.getByText('Copyright © 2024 Opentrons') + expect( + screen.getByRole('link', { name: 'privacy policy' }) + ).toHaveAttribute('href', 'https://opentrons.com/privacy-policy') + expect( + screen.getByRole('link', { name: 'end user license agreement' }) + ).toHaveAttribute('href', 'https://opentrons.com/eula') + }) +}) diff --git a/components/src/organisms/EndUserAgreementFooter/index.tsx b/components/src/organisms/EndUserAgreementFooter/index.tsx new file mode 100644 index 00000000000..5e40b205665 --- /dev/null +++ b/components/src/organisms/EndUserAgreementFooter/index.tsx @@ -0,0 +1,49 @@ +import { StyledText } from '../../atoms' +import { COLORS } from '../../helix-design-system' +import { Flex, Link } from '../../primitives' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + TEXT_DECORATION_UNDERLINE, +} from '../../styles' +import { SPACING } from '../../ui-style-constants' + +const PRIVACY_POLICY_URL = 'https://opentrons.com/privacy-policy' +const EULA_URL = 'https://opentrons.com/eula' + +export function EndUserAgreementFooter(): JSX.Element { + return ( + + + By continuing, you agree to the Opentrons{' '} + + privacy policy + {' '} + and{' '} + + end user license agreement + + + + Copyright © 2024 Opentrons + + + ) +} diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index 4346806b861..e7437e1e57f 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -76,18 +76,18 @@ export function Toolbox(props: ToolboxProps): JSX.Element { ...(side === 'left' && { left: '0' }), ...(horizontalSide === 'bottom' && { bottom: '0' }), ...(horizontalSide === 'top' && { top: '5rem' }), + zIndex: 10, } : {} return ( { beforeEach(() => { - cy.visit('/') - cy.viewport('macbook-15') + navigateToUrl('/') }) it('successfully loads', () => { diff --git a/labware-library/cypress/e2e/labware-creator/create.cy.js b/labware-library/cypress/e2e/labware-creator/create.cy.js index 299a3444a86..917b263cb78 100644 --- a/labware-library/cypress/e2e/labware-creator/create.cy.js +++ b/labware-library/cypress/e2e/labware-creator/create.cy.js @@ -2,10 +2,11 @@ // an element is in view before clicking or checking with // { force: true } +import { navigateToUrl } from '../../support/e2e' + context('The Labware Creator Landing Page', () => { beforeEach(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) describe('The initial text', () => { diff --git a/labware-library/cypress/e2e/labware-creator/customTubeRack.cy.js b/labware-library/cypress/e2e/labware-creator/customTubeRack.cy.js index 319e7f4ea81..b7f9cbbcc30 100644 --- a/labware-library/cypress/e2e/labware-creator/customTubeRack.cy.js +++ b/labware-library/cypress/e2e/labware-creator/customTubeRack.cy.js @@ -1,13 +1,13 @@ -import 'cypress-file-upload' -import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' - -const expectedExportFixture = - '../fixtures/somerackbrand_24_tuberack_1500ul.json' +import { + navigateToUrl, + fileHelper, + wellBottomImageLocator, +} from '../../support/e2e' +const fileHolder = fileHelper('somerackbrand_24_tuberack_1500ul') context('Tubes and Rack', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) describe('Custom 6 x 4 tube rack', () => { @@ -109,24 +109,29 @@ context('Tubes and Rack', () => { cy.contains('Diameter is a required field').should('not.exist') // well bottom shape and depth + // check flat cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') + + // check u shaped cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') + + // check v shaped cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('100').blur() @@ -159,28 +164,20 @@ context('Tubes and Rack', () => { cy.get( "input[placeholder='somerackbrand 24 Tube Rack with sometubebrand 1.5 mL']" ).should('exist') - cy.get("input[placeholder='somerackbrand_24_tuberack_1500ul']").should( + cy.get(`input[placeholder='${fileHolder.downloadFileStem}']`).should( 'exist' ) - - // now try again with all fields inputed - cy.fixture(expectedExportFixture).then(expectedExportLabwareDef => { - cy.get('button').contains('EXPORT FILE').click() - - cy.window() - .its('__lastSavedFileBlob__') - .should('be.a', 'blob') - .should(async blob => { - const labwareDefText = await blob.text() - const savedDef = JSON.parse(labwareDefText) - - expectDeepEqual(assert, savedDef, expectedExportLabwareDef) + cy.get('button').contains('EXPORT FILE').click() + + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then(actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal( + expectedExportLabwareDef + ) }) - - cy.window() - .its('__lastSavedFileName__') - .should('equal', `somerackbrand_24_tuberack_1500ul.json`) - }) + } + ) }) }) }) diff --git a/labware-library/cypress/e2e/labware-creator/fileImport.cy.js b/labware-library/cypress/e2e/labware-creator/fileImport.cy.js index e0fc480107f..616edca7d5b 100644 --- a/labware-library/cypress/e2e/labware-creator/fileImport.cy.js +++ b/labware-library/cypress/e2e/labware-creator/fileImport.cy.js @@ -1,11 +1,15 @@ -import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' +import { + navigateToUrl, + fileHelper, + wellBottomImageLocator, +} from '../../support/e2e' +const fileHolder = fileHelper('testpro_15_wellplate_5ul') const importedLabwareFile = 'TestLabwareDefinition.json' describe('File Import', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) it('tests the file import flow', () => { @@ -49,9 +53,9 @@ describe('File Import', () => { // verify well bottom and depth cy.get("input[name='wellBottomShape'][value='flat']").should('exist') - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellDepth'][value='5']").should('exist') // verify grid spacing @@ -69,7 +73,9 @@ describe('File Import', () => { // File info cy.get("input[placeholder='TestPro 15 Well Plate 5 µL']").should('exist') - cy.get("input[placeholder='testpro_15_wellplate_5ul']").should('exist') + cy.get(`input[placeholder='${fileHolder.downloadFileStem}']`).should( + 'exist' + ) // All fields present cy.get('button[class*="_export_button_"]').click({ force: true }) @@ -77,20 +83,12 @@ describe('File Import', () => { 'Please resolve all invalid fields in order to export the labware definition' ).should('not.exist') - cy.fixture(importedLabwareFile).then(expected => { - cy.window() - .its('__lastSavedFileBlob__') - .should('be.a', 'blob') // wait until we get the blob - .should(async blob => { - const labwareDefText = await blob.text() - const savedDef = JSON.parse(labwareDefText) - - expectDeepEqual(assert, savedDef, expected) + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then(actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal(expectedExportLabwareDef) }) - }) - - cy.window() - .its('__lastSavedFileName__') - .should('equal', 'testpro_15_wellplate_5ul.json') + } + ) }) }) diff --git a/labware-library/cypress/e2e/labware-creator/reservoir.cy.js b/labware-library/cypress/e2e/labware-creator/reservoir.cy.js index 75197208859..32b18c88a40 100644 --- a/labware-library/cypress/e2e/labware-creator/reservoir.cy.js +++ b/labware-library/cypress/e2e/labware-creator/reservoir.cy.js @@ -1,11 +1,13 @@ -// Scrolling seems wonky, so I disabled checking to see if -// an element is in view before clicking or checking with -// { force: true } +import { + navigateToUrl, + fileHelper, + wellBottomImageLocator, +} from '../../support/e2e' +const fileHolder = fileHelper('testpro_10_reservoir_250ul') context('Reservoirs', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) describe('Reservoir', () => { @@ -143,21 +145,21 @@ context('Reservoirs', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('70').blur() @@ -198,13 +200,24 @@ context('Reservoirs', () => { // File info cy.get("input[placeholder='TestPro 10 Reservoir 250 µL']").should('exist') - cy.get("input[placeholder='testpro_10_reservoir_250ul']").should('exist') + cy.get(`input[placeholder='${fileHolder.downloadFileStem}']`).should( + 'exist' + ) // All fields present cy.get('button[class*="_export_button_"]').click({ force: true }) cy.contains( 'Please resolve all invalid fields in order to export the labware definition' ).should('not.exist') + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then(actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal( + expectedExportLabwareDef + ) + }) + } + ) }) }) }) diff --git a/labware-library/cypress/e2e/labware-creator/tipRack.cy.js b/labware-library/cypress/e2e/labware-creator/tipRack.cy.js index e69e3dd7285..4d27a47effc 100644 --- a/labware-library/cypress/e2e/labware-creator/tipRack.cy.js +++ b/labware-library/cypress/e2e/labware-creator/tipRack.cy.js @@ -1,12 +1,9 @@ -import 'cypress-file-upload' -import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' - -const expectedExportFixture = '../fixtures/generic_1_tiprack_20ul.json' +import { navigateToUrl, fileHelper } from '../../support/e2e' +const fileHolder = fileHelper('generic_1_tiprack_20ul') describe('Create a Tip Rack', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) it('Should create a tip rack', () => { // Tip Rack Selection from drop down @@ -242,26 +239,19 @@ describe('Create a Tip Rack', () => { cy.get('input[name="displayName"]') .clear() .type('Brand Chalu 1 Tip Rack 20ul') - cy.get('input[name="loadName"]').clear().type('generic_1_tiprack_20ul') + cy.get('input[name="loadName"]').clear().type(fileHolder.downloadFileStem) // Verify the exported file to the fixture cy.get('button').contains('EXPORT FILE').click() - cy.fixture(expectedExportFixture).then(expectedExportLabwareDef => { - cy.window() - .its('__lastSavedFileBlob__') - .should('be.a', 'blob') - .should(async blob => { - const labwareDefText = await blob.text() - const savedDef = JSON.parse(labwareDefText) - - expectDeepEqual(assert, savedDef, expectedExportLabwareDef) + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then(actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal(expectedExportLabwareDef) }) - }) + } + ) - cy.window() - .its('__lastSavedFileName__') - .should('equal', `generic_1_tiprack_20ul.json`) // 'verify the too big, too small error cy.get('input[name="gridOffsetY"]').clear().type('24') cy.get('#CheckYourWork span') diff --git a/labware-library/cypress/e2e/labware-creator/tubesBlock.cy.js b/labware-library/cypress/e2e/labware-creator/tubesBlock.cy.js index 66ea8d0dedc..4240342390a 100644 --- a/labware-library/cypress/e2e/labware-creator/tubesBlock.cy.js +++ b/labware-library/cypress/e2e/labware-creator/tubesBlock.cy.js @@ -1,12 +1,13 @@ -// Scrolling seems wonky, so I disabled checking to see if -// an element is in view before clicking or checking with -// { force: true } +import { + navigateToUrl, + fileHelper, + wellBottomImageLocator, +} from '../../support/e2e' +const fileHolder = fileHelper('testpro_24_aluminumblock_10ul') context('Tubes and Block', () => { beforeEach(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') - + navigateToUrl('/#/create') cy.get('label') .contains('What type of labware are you creating?') .children() @@ -106,21 +107,21 @@ context('Tubes and Block', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -232,21 +233,21 @@ context('Tubes and Block', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -383,21 +384,21 @@ context('Tubes and Block', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -445,8 +446,7 @@ context('Tubes and Block', () => { }) it('tests the whole form and file export', () => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') cy.get('label') .contains('What type of labware are you creating?') .children() @@ -533,21 +533,21 @@ context('Tubes and Block', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -575,7 +575,7 @@ context('Tubes and Block', () => { cy.get("input[placeholder='TestPro 24 Aluminum Block 10 µL']").should( 'exist' ) - cy.get("input[placeholder='testpro_24_aluminumblock_10ul']").should( + cy.get(`input[placeholder='${fileHolder.downloadFileStem}']`).should( 'exist' ) @@ -584,6 +584,18 @@ context('Tubes and Block', () => { cy.contains( 'Please resolve all invalid fields in order to export the labware definition' ).should('not.exist') + + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then( + actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal( + expectedExportLabwareDef + ) + } + ) + } + ) }) }) }) diff --git a/labware-library/cypress/e2e/labware-creator/tubesRack.cy.js b/labware-library/cypress/e2e/labware-creator/tubesRack.cy.js index 4214f215dc0..0409221b6a4 100644 --- a/labware-library/cypress/e2e/labware-creator/tubesRack.cy.js +++ b/labware-library/cypress/e2e/labware-creator/tubesRack.cy.js @@ -1,12 +1,9 @@ -// Scrolling seems wonky, so I disabled checking to see if -// an element is in view before clicking or checking with -// { force: true } +import { navigateToUrl, wellBottomImageLocator } from '../../support/e2e' context('Tubes and Rack', () => { describe('Six tubes', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') cy.get('label') .contains('What type of labware are you creating?') .children() @@ -95,21 +92,21 @@ context('Tubes and Rack', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -137,9 +134,7 @@ context('Tubes and Rack', () => { describe('Fifteen tubes', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') - + navigateToUrl('#/create') cy.get('label') .contains('What type of labware are you creating?') .children() @@ -226,21 +221,21 @@ context('Tubes and Rack', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -268,9 +263,7 @@ context('Tubes and Rack', () => { describe('Twentyfour tubes', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') - + navigateToUrl('/#/create') cy.get('label') .contains('What type of labware are you creating?') .children() @@ -356,21 +349,21 @@ context('Tubes and Rack', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() diff --git a/labware-library/cypress/e2e/labware-creator/wellPlate.cy.js b/labware-library/cypress/e2e/labware-creator/wellPlate.cy.js index 5b27cfcfd72..df12cf153a5 100644 --- a/labware-library/cypress/e2e/labware-creator/wellPlate.cy.js +++ b/labware-library/cypress/e2e/labware-creator/wellPlate.cy.js @@ -2,14 +2,16 @@ // that cannot be imported. The creator probably shouldn't allow // a user to do this. -// Scrolling seems wonky, so I disabled checking to see if -// an element is in view before clicking or checking with -// { force: true } +import { + navigateToUrl, + fileHelper, + wellBottomImageLocator, +} from '../../support/e2e' +const fileHolder = fileHelper('testpro_80_wellplate_100ul') context('Well Plates', () => { before(() => { - cy.visit('/#/create') - cy.viewport('macbook-15') + navigateToUrl('/#/create') }) describe('Create a well plate', () => { @@ -145,21 +147,21 @@ context('Well Plates', () => { cy.get("input[name='wellBottomShape'][value='flat']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='u']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('exist') - cy.get("img[src*='_v.']").should('not.exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('exist') + cy.get(wellBottomImageLocator.v).should('not.exist') cy.get("input[name='wellBottomShape'][value='v']").check({ force: true, }) - cy.get("img[src*='_flat.']").should('not.exist') - cy.get("img[src*='_round.']").should('not.exist') - cy.get("img[src*='_v.']").should('exist') + cy.get(wellBottomImageLocator.flat).should('not.exist') + cy.get(wellBottomImageLocator.round).should('not.exist') + cy.get(wellBottomImageLocator.v).should('exist') cy.get("input[name='wellDepth']").focus().blur() cy.contains('Depth is a required field').should('exist') cy.get("input[name='wellDepth']").type('10').blur() @@ -208,7 +210,9 @@ context('Well Plates', () => { cy.get("input[placeholder='TestPro 80 Well Plate 100 µL']").should( 'exist' ) - cy.get("input[placeholder='testpro_80_wellplate_100ul']").should('exist') + cy.get(`input[placeholder='${fileHolder.downloadFileStem}']`).should( + 'exist' + ) // All fields present cy.get('button[class*="_export_button_"]').click({ force: true }) @@ -216,7 +220,15 @@ context('Well Plates', () => { 'Please resolve all invalid fields in order to export the labware definition' ).should('not.exist') - // TODO IMMEDIATELY match against fixture ??? Is this not happening? + cy.fixture(fileHolder.expectedExportFixture).then( + expectedExportLabwareDef => { + cy.readFile(fileHolder.downloadPath).then(actualExportLabwareDef => { + expect(actualExportLabwareDef).to.deep.equal( + expectedExportLabwareDef + ) + }) + } + ) }) }) }) diff --git a/labware-library/cypress/e2e/navigation.cy.js b/labware-library/cypress/e2e/navigation.cy.js index 83ce2dd7369..0b4c3c14a40 100644 --- a/labware-library/cypress/e2e/navigation.cy.js +++ b/labware-library/cypress/e2e/navigation.cy.js @@ -1,7 +1,8 @@ +import { navigateToUrl } from '../support/e2e' + describe('Desktop Navigation', () => { beforeEach(() => { - cy.visit('/') - cy.viewport('macbook-15') + navigateToUrl('/') }) it('contains the subdomain nav bar', () => { diff --git a/labware-library/cypress/fixtures/testpro_10_reservoir_250ul.json b/labware-library/cypress/fixtures/testpro_10_reservoir_250ul.json new file mode 100644 index 00000000000..5941e1b3e5e --- /dev/null +++ b/labware-library/cypress/fixtures/testpro_10_reservoir_250ul.json @@ -0,0 +1,154 @@ +{ + "ordering": [ + ["A1"], + ["A2"], + ["A3"], + ["A4"], + ["A5"], + ["A6"], + ["A7"], + ["A8"], + ["A9"], + ["A10"] + ], + "brand": { + "brand": "TestPro", + "brandId": ["001"] + }, + "metadata": { + "displayName": "TestPro 10 Reservoir 250 µL", + "displayCategory": "reservoir", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 75 + }, + "wells": { + "A1": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 10, + "y": 40, + "z": 5 + }, + "A2": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 22, + "y": 40, + "z": 5 + }, + "A3": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 34, + "y": 40, + "z": 5 + }, + "A4": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 46, + "y": 40, + "z": 5 + }, + "A5": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 58, + "y": 40, + "z": 5 + }, + "A6": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 70, + "y": 40, + "z": 5 + }, + "A7": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 82, + "y": 40, + "z": 5 + }, + "A8": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 94, + "y": 40, + "z": 5 + }, + "A9": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 106, + "y": 40, + "z": 5 + }, + "A10": { + "depth": 70, + "totalLiquidVolume": 250, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 60, + "x": 118, + "y": 40, + "z": 5 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10"] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "testpro_10_reservoir_250ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/labware-library/cypress/fixtures/testpro_15_wellplate_5ul.json b/labware-library/cypress/fixtures/testpro_15_wellplate_5ul.json new file mode 100644 index 00000000000..6eac7bd5fc6 --- /dev/null +++ b/labware-library/cypress/fixtures/testpro_15_wellplate_5ul.json @@ -0,0 +1,200 @@ +{ + "ordering": [ + ["A1", "B1", "C1"], + ["A2", "B2", "C2"], + ["A3", "B3", "C3"], + ["A4", "B4", "C4"], + ["A5", "B5", "C5"] + ], + "brand": { + "brand": "TestPro", + "brandId": ["001"] + }, + "metadata": { + "displayName": "TestPro 15 Well Plate 5 µL", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 5 + }, + "wells": { + "A1": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 10, + "y": 75, + "z": 0 + }, + "B1": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 10, + "y": 50, + "z": 0 + }, + "C1": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 10, + "y": 25, + "z": 0 + }, + "A2": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 35, + "y": 75, + "z": 0 + }, + "B2": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 35, + "y": 50, + "z": 0 + }, + "C2": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 35, + "y": 25, + "z": 0 + }, + "A3": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 60, + "y": 75, + "z": 0 + }, + "B3": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 60, + "y": 50, + "z": 0 + }, + "C3": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 60, + "y": 25, + "z": 0 + }, + "A4": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 85, + "y": 75, + "z": 0 + }, + "B4": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 85, + "y": 50, + "z": 0 + }, + "C4": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 85, + "y": 25, + "z": 0 + }, + "A5": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 110, + "y": 75, + "z": 0 + }, + "B5": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 110, + "y": 50, + "z": 0 + }, + "C5": { + "depth": 5, + "totalLiquidVolume": 5, + "shape": "circular", + "diameter": 5, + "x": 110, + "y": 25, + "z": 0 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "flat" + }, + "wells": [ + "A1", + "B1", + "C1", + "A2", + "B2", + "C2", + "A3", + "B3", + "C3", + "A4", + "B4", + "C4", + "A5", + "B5", + "C5" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "testpro_15_wellplate_5ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/labware-library/cypress/fixtures/testpro_24_aluminumblock_10ul.json b/labware-library/cypress/fixtures/testpro_24_aluminumblock_10ul.json new file mode 100644 index 00000000000..d653e918f90 --- /dev/null +++ b/labware-library/cypress/fixtures/testpro_24_aluminumblock_10ul.json @@ -0,0 +1,316 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1"], + ["A2", "B2", "C2", "D2"], + ["A3", "B3", "C3", "D3"], + ["A4", "B4", "C4", "D4"], + ["A5", "B5", "C5", "D5"], + ["A6", "B6", "C6", "D6"] + ], + "brand": { + "brand": "TestPro", + "brandId": ["001"] + }, + "metadata": { + "displayName": "TestPro 24 Aluminum Block 10 µL", + "displayCategory": "aluminumBlock", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.5, + "zDimension": 75 + }, + "wells": { + "A1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 20.75, + "y": 68.63, + "z": 65 + }, + "B1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 20.75, + "y": 51.38, + "z": 65 + }, + "C1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 20.75, + "y": 34.13, + "z": 65 + }, + "D1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 20.75, + "y": 16.88, + "z": 65 + }, + "A2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 38, + "y": 68.63, + "z": 65 + }, + "B2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 38, + "y": 51.38, + "z": 65 + }, + "C2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 38, + "y": 34.13, + "z": 65 + }, + "D2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 38, + "y": 16.88, + "z": 65 + }, + "A3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 55.25, + "y": 68.63, + "z": 65 + }, + "B3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 55.25, + "y": 51.38, + "z": 65 + }, + "C3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 55.25, + "y": 34.13, + "z": 65 + }, + "D3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 55.25, + "y": 16.88, + "z": 65 + }, + "A4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 72.5, + "y": 68.63, + "z": 65 + }, + "B4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 72.5, + "y": 51.38, + "z": 65 + }, + "C4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 72.5, + "y": 34.13, + "z": 65 + }, + "D4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 72.5, + "y": 16.88, + "z": 65 + }, + "A5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 89.75, + "y": 68.63, + "z": 65 + }, + "B5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 89.75, + "y": 51.38, + "z": 65 + }, + "C5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 89.75, + "y": 34.13, + "z": 65 + }, + "D5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 89.75, + "y": 16.88, + "z": 65 + }, + "A6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 107, + "y": 68.63, + "z": 65 + }, + "B6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 107, + "y": 51.38, + "z": 65 + }, + "C6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 107, + "y": 34.13, + "z": 65 + }, + "D6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 107, + "y": 16.88, + "z": 65 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v", + "displayCategory": "tubeRack" + }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "A2", + "B2", + "C2", + "D2", + "A3", + "B3", + "C3", + "D3", + "A4", + "B4", + "C4", + "D4", + "A5", + "B5", + "C5", + "D5", + "A6", + "B6", + "C6", + "D6" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "testpro_24_aluminumblock_10ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/labware-library/cypress/fixtures/testpro_80_wellplate_100ul.json b/labware-library/cypress/fixtures/testpro_80_wellplate_100ul.json new file mode 100644 index 00000000000..f51b575836a --- /dev/null +++ b/labware-library/cypress/fixtures/testpro_80_wellplate_100ul.json @@ -0,0 +1,935 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + ], + "brand": { + "brand": "TestPro", + "brandId": ["001"] + }, + "metadata": { + "displayName": "TestPro 80 Well Plate 100 µL", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 75 + }, + "wells": { + "A1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 77, + "z": 65 + }, + "B1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 67, + "z": 65 + }, + "C1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 57, + "z": 65 + }, + "D1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 47, + "z": 65 + }, + "E1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 37, + "z": 65 + }, + "F1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 27, + "z": 65 + }, + "G1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 17, + "z": 65 + }, + "H1": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 10, + "y": 7, + "z": 65 + }, + "A2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 77, + "z": 65 + }, + "B2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 67, + "z": 65 + }, + "C2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 57, + "z": 65 + }, + "D2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 47, + "z": 65 + }, + "E2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 37, + "z": 65 + }, + "F2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 27, + "z": 65 + }, + "G2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 17, + "z": 65 + }, + "H2": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 22, + "y": 7, + "z": 65 + }, + "A3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 77, + "z": 65 + }, + "B3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 67, + "z": 65 + }, + "C3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 57, + "z": 65 + }, + "D3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 47, + "z": 65 + }, + "E3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 37, + "z": 65 + }, + "F3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 27, + "z": 65 + }, + "G3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 17, + "z": 65 + }, + "H3": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 34, + "y": 7, + "z": 65 + }, + "A4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 77, + "z": 65 + }, + "B4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 67, + "z": 65 + }, + "C4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 57, + "z": 65 + }, + "D4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 47, + "z": 65 + }, + "E4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 37, + "z": 65 + }, + "F4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 27, + "z": 65 + }, + "G4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 17, + "z": 65 + }, + "H4": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 46, + "y": 7, + "z": 65 + }, + "A5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 77, + "z": 65 + }, + "B5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 67, + "z": 65 + }, + "C5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 57, + "z": 65 + }, + "D5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 47, + "z": 65 + }, + "E5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 37, + "z": 65 + }, + "F5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 27, + "z": 65 + }, + "G5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 17, + "z": 65 + }, + "H5": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 58, + "y": 7, + "z": 65 + }, + "A6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 77, + "z": 65 + }, + "B6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 67, + "z": 65 + }, + "C6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 57, + "z": 65 + }, + "D6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 47, + "z": 65 + }, + "E6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 37, + "z": 65 + }, + "F6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 27, + "z": 65 + }, + "G6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 17, + "z": 65 + }, + "H6": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 70, + "y": 7, + "z": 65 + }, + "A7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 77, + "z": 65 + }, + "B7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 67, + "z": 65 + }, + "C7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 57, + "z": 65 + }, + "D7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 47, + "z": 65 + }, + "E7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 37, + "z": 65 + }, + "F7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 27, + "z": 65 + }, + "G7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 17, + "z": 65 + }, + "H7": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 82, + "y": 7, + "z": 65 + }, + "A8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 77, + "z": 65 + }, + "B8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 67, + "z": 65 + }, + "C8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 57, + "z": 65 + }, + "D8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 47, + "z": 65 + }, + "E8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 37, + "z": 65 + }, + "F8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 27, + "z": 65 + }, + "G8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 17, + "z": 65 + }, + "H8": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 94, + "y": 7, + "z": 65 + }, + "A9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 77, + "z": 65 + }, + "B9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 67, + "z": 65 + }, + "C9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 57, + "z": 65 + }, + "D9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 47, + "z": 65 + }, + "E9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 37, + "z": 65 + }, + "F9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 27, + "z": 65 + }, + "G9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 17, + "z": 65 + }, + "H9": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 106, + "y": 7, + "z": 65 + }, + "A10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 77, + "z": 65 + }, + "B10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 67, + "z": 65 + }, + "C10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 57, + "z": 65 + }, + "D10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 47, + "z": 65 + }, + "E10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 37, + "z": 65 + }, + "F10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 27, + "z": 65 + }, + "G10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 17, + "z": 65 + }, + "H10": { + "depth": 10, + "totalLiquidVolume": 100, + "shape": "rectangular", + "xDimension": 8, + "yDimension": 8, + "x": 118, + "y": 7, + "z": 65 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "testpro_80_wellplate_100ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/labware-library/cypress/fixtures/testpro_96_aluminumblock_10ul.json b/labware-library/cypress/fixtures/testpro_96_aluminumblock_10ul.json new file mode 100644 index 00000000000..0b99d24def6 --- /dev/null +++ b/labware-library/cypress/fixtures/testpro_96_aluminumblock_10ul.json @@ -0,0 +1,1114 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "TestPro", + "brandId": ["001"] + }, + "metadata": { + "displayName": "TestPro 96 Aluminum Block 10 µL", + "displayCategory": "aluminumBlock", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.5, + "zDimension": 75 + }, + "wells": { + "A1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 74.25, + "z": 65 + }, + "B1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 65.25, + "z": 65 + }, + "C1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 56.25, + "z": 65 + }, + "D1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 47.25, + "z": 65 + }, + "E1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 38.25, + "z": 65 + }, + "F1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 29.25, + "z": 65 + }, + "G1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 20.25, + "z": 65 + }, + "H1": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 14.38, + "y": 11.25, + "z": 65 + }, + "A2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 74.25, + "z": 65 + }, + "B2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 65.25, + "z": 65 + }, + "C2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 56.25, + "z": 65 + }, + "D2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 47.25, + "z": 65 + }, + "E2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 38.25, + "z": 65 + }, + "F2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 29.25, + "z": 65 + }, + "G2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 20.25, + "z": 65 + }, + "H2": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 23.38, + "y": 11.25, + "z": 65 + }, + "A3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 74.25, + "z": 65 + }, + "B3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 65.25, + "z": 65 + }, + "C3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 56.25, + "z": 65 + }, + "D3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 47.25, + "z": 65 + }, + "E3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 38.25, + "z": 65 + }, + "F3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 29.25, + "z": 65 + }, + "G3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 20.25, + "z": 65 + }, + "H3": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 32.38, + "y": 11.25, + "z": 65 + }, + "A4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 74.25, + "z": 65 + }, + "B4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 65.25, + "z": 65 + }, + "C4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 56.25, + "z": 65 + }, + "D4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 47.25, + "z": 65 + }, + "E4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 38.25, + "z": 65 + }, + "F4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 29.25, + "z": 65 + }, + "G4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 20.25, + "z": 65 + }, + "H4": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 41.38, + "y": 11.25, + "z": 65 + }, + "A5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 74.25, + "z": 65 + }, + "B5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 65.25, + "z": 65 + }, + "C5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 56.25, + "z": 65 + }, + "D5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 47.25, + "z": 65 + }, + "E5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 38.25, + "z": 65 + }, + "F5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 29.25, + "z": 65 + }, + "G5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 20.25, + "z": 65 + }, + "H5": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 50.38, + "y": 11.25, + "z": 65 + }, + "A6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 74.25, + "z": 65 + }, + "B6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 65.25, + "z": 65 + }, + "C6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 56.25, + "z": 65 + }, + "D6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 47.25, + "z": 65 + }, + "E6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 38.25, + "z": 65 + }, + "F6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 29.25, + "z": 65 + }, + "G6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 20.25, + "z": 65 + }, + "H6": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 59.38, + "y": 11.25, + "z": 65 + }, + "A7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 74.25, + "z": 65 + }, + "B7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 65.25, + "z": 65 + }, + "C7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 56.25, + "z": 65 + }, + "D7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 47.25, + "z": 65 + }, + "E7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 38.25, + "z": 65 + }, + "F7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 29.25, + "z": 65 + }, + "G7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 20.25, + "z": 65 + }, + "H7": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 68.38, + "y": 11.25, + "z": 65 + }, + "A8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 74.25, + "z": 65 + }, + "B8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 65.25, + "z": 65 + }, + "C8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 56.25, + "z": 65 + }, + "D8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 47.25, + "z": 65 + }, + "E8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 38.25, + "z": 65 + }, + "F8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 29.25, + "z": 65 + }, + "G8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 20.25, + "z": 65 + }, + "H8": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 77.38, + "y": 11.25, + "z": 65 + }, + "A9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 74.25, + "z": 65 + }, + "B9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 65.25, + "z": 65 + }, + "C9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 56.25, + "z": 65 + }, + "D9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 47.25, + "z": 65 + }, + "E9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 38.25, + "z": 65 + }, + "F9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 29.25, + "z": 65 + }, + "G9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 20.25, + "z": 65 + }, + "H9": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 86.38, + "y": 11.25, + "z": 65 + }, + "A10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 74.25, + "z": 65 + }, + "B10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 65.25, + "z": 65 + }, + "C10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 56.25, + "z": 65 + }, + "D10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 47.25, + "z": 65 + }, + "E10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 38.25, + "z": 65 + }, + "F10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 29.25, + "z": 65 + }, + "G10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 20.25, + "z": 65 + }, + "H10": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 95.38, + "y": 11.25, + "z": 65 + }, + "A11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 74.25, + "z": 65 + }, + "B11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 65.25, + "z": 65 + }, + "C11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 56.25, + "z": 65 + }, + "D11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 47.25, + "z": 65 + }, + "E11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 38.25, + "z": 65 + }, + "F11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 29.25, + "z": 65 + }, + "G11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 20.25, + "z": 65 + }, + "H11": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 104.38, + "y": 11.25, + "z": 65 + }, + "A12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 74.25, + "z": 65 + }, + "B12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 65.25, + "z": 65 + }, + "C12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 56.25, + "z": 65 + }, + "D12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 47.25, + "z": 65 + }, + "E12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 38.25, + "z": 65 + }, + "F12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 29.25, + "z": 65 + }, + "G12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 20.25, + "z": 65 + }, + "H12": { + "depth": 10, + "totalLiquidVolume": 10, + "shape": "rectangular", + "xDimension": 10, + "yDimension": 10, + "x": 113.38, + "y": 11.25, + "z": 65 + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v", + "displayCategory": "wellPlate" + }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "testpro_96_aluminumblock_10ul" + }, + "namespace": "custom_beta", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/labware-library/cypress/mocks/file-saver.js b/labware-library/cypress/mocks/file-saver.js deleted file mode 100644 index d4c7febe539..00000000000 --- a/labware-library/cypress/mocks/file-saver.js +++ /dev/null @@ -1,6 +0,0 @@ -// mock for 'file-saver' npm module - -export const saveAs = (blob, fileName) => { - global.__lastSavedFileBlob__ = blob - global.__lastSavedFileName__ = fileName -} diff --git a/labware-library/cypress/plugins/index.js b/labware-library/cypress/plugins/index.js deleted file mode 100644 index f392875c7d9..00000000000 --- a/labware-library/cypress/plugins/index.js +++ /dev/null @@ -1,23 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -// eslint-disable-next-line no-unused-vars -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -} diff --git a/labware-library/cypress/support/e2e.js b/labware-library/cypress/support/e2e.js deleted file mode 100644 index d68db96df26..00000000000 --- a/labware-library/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js 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') diff --git a/labware-library/cypress/support/e2e.ts b/labware-library/cypress/support/e2e.ts new file mode 100644 index 00000000000..85dcff19ba8 --- /dev/null +++ b/labware-library/cypress/support/e2e.ts @@ -0,0 +1,40 @@ +// *********************************************************** +// This file runs before every single spec file. +// We do this purely as a convenience mechanism so you don't have to import this file. +// https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Support-file +// *********************************************************** +import { join } from 'path' +import './commands' + +export const navigateToUrl = (url: string): void => { + cy.visit(url) + cy.viewport('macbook-15') +} + +export const wellBottomImageLocator: Record = { + flat: 'img[alt*="flat bottom"]', + round: 'img[alt*="u shaped"]', + v: 'img[alt*="v shaped"]', +} + +interface FileHelperResult { + downloadsFolder: string + downloadFileStem: string + downloadFilename: string + downloadPath: string + expectedExportFixture: string +} + +export const fileHelper = (fileStem: string): FileHelperResult => { + const downloadsFolder = Cypress.config('downloadsFolder') + const downloadFileStem = fileStem + const downloadFilename = `${downloadFileStem}.json` + const downloadPath = join(downloadsFolder, downloadFilename) + return { + downloadsFolder, + downloadFileStem, + downloadFilename, + downloadPath, + expectedExportFixture: `../fixtures/${downloadFilename}`, + } +} diff --git a/labware-library/src/components/labware-ui/labware-images.ts b/labware-library/src/components/labware-ui/labware-images.ts index 36fe2cb8dfb..8df00e07f2a 100644 --- a/labware-library/src/components/labware-ui/labware-images.ts +++ b/labware-library/src/components/labware-ui/labware-images.ts @@ -468,4 +468,13 @@ export const labwareImages: Record = { import.meta.url ).href, ], + opentrons_tough_pcr_auto_sealing_lid: [ + new URL( + '../../images/opentrons_tough_pcr_auto_sealing_lid.jpg', + import.meta.url + ).href, + ], + opentrons_flex_deck_riser: [ + new URL('../../images/opentrons_flex_deck_riser.jpg', import.meta.url).href, + ], } diff --git a/labware-library/src/images/opentrons_flex_deck_riser.jpg b/labware-library/src/images/opentrons_flex_deck_riser.jpg new file mode 100644 index 00000000000..b8576833538 Binary files /dev/null and b/labware-library/src/images/opentrons_flex_deck_riser.jpg differ diff --git a/labware-library/src/images/opentrons_tough_pcr_auto_sealing_lid.jpg b/labware-library/src/images/opentrons_tough_pcr_auto_sealing_lid.jpg new file mode 100644 index 00000000000..a81c42b2d2c Binary files /dev/null and b/labware-library/src/images/opentrons_tough_pcr_auto_sealing_lid.jpg differ diff --git a/labware-library/src/localization/en.ts b/labware-library/src/localization/en.ts index 07e7bda76d1..9745ed44fb2 100644 --- a/labware-library/src/localization/en.ts +++ b/labware-library/src/localization/en.ts @@ -10,6 +10,7 @@ export const CATEGORY_LABELS_BY_CATEGORY = { trash: 'Trash', other: 'Other', adapter: 'Adapter', + lid: 'Lid', } export const PLURAL_CATEGORY_LABELS_BY_CATEGORY = { @@ -20,6 +21,7 @@ export const PLURAL_CATEGORY_LABELS_BY_CATEGORY = { wellPlate: 'Well Plates', reservoir: 'Reservoirs', aluminumBlock: 'Aluminum Blocks', + lid: 'Lid', trash: 'Trashes', other: 'Other', } diff --git a/labware-library/vite.config.mts b/labware-library/vite.config.mts index 43d5065c011..0c05338af06 100644 --- a/labware-library/vite.config.mts +++ b/labware-library/vite.config.mts @@ -8,14 +8,6 @@ import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' import { cssModuleSideEffect } from './cssModuleSideEffect' -const testAliases: {} | { 'file-saver': string } = - process.env.CYPRESS === '1' - ? { - 'file-saver': - path.resolve(__dirname, 'cypress/mocks/file-saver.js') ?? '', - } - : {} - export default defineConfig({ // this makes imports relative rather than absolute base: '', @@ -70,7 +62,6 @@ export default defineConfig({ '@opentrons/step-generation': path.resolve( '../step-generation/src/index.ts' ), - ...testAliases, }, }, server: { diff --git a/opentrons-ai-client/src/App.test.tsx b/opentrons-ai-client/src/App.test.tsx index 859bb488f0e..ec61b02472c 100644 --- a/opentrons-ai-client/src/App.test.tsx +++ b/opentrons-ai-client/src/App.test.tsx @@ -1,22 +1,13 @@ -import { fireEvent, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach, expect } from 'vitest' -import * as auth0 from '@auth0/auth0-react' import { renderWithProviders } from './__testing-utils__' import { i18n } from './i18n' -import { SidePanel } from './molecules/SidePanel' -import { MainContentContainer } from './organisms/MainContentContainer' -import { Loading } from './molecules/Loading' import { App } from './App' +import { OpentronsAI } from './OpentronsAI' -vi.mock('@auth0/auth0-react') - -const mockLogout = vi.fn() - -vi.mock('./molecules/SidePanel') -vi.mock('./organisms/MainContentContainer') -vi.mock('./molecules/Loading') +vi.mock('./OpentronsAI') const render = (): ReturnType => { return renderWithProviders(, { @@ -26,42 +17,11 @@ const render = (): ReturnType => { describe('App', () => { beforeEach(() => { - vi.mocked(SidePanel).mockReturnValue(
mock SidePanel
) - vi.mocked(MainContentContainer).mockReturnValue( -
mock MainContentContainer
- ) - vi.mocked(Loading).mockReturnValue(
mock Loading
) - }) - - it('should render loading screen when isLoading is true', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: false, - isLoading: true, - }) - render() - screen.getByText('mock Loading') - }) - - it('should render text', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: true, - isLoading: false, - }) - render() - screen.getByText('mock SidePanel') - screen.getByText('mock MainContentContainer') - screen.getByText('Logout') + vi.mocked(OpentronsAI).mockReturnValue(
mock OpentronsAI
) }) - it('should call a mock function when clicking logout button', () => { - ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ - isAuthenticated: true, - isLoading: false, - logout: mockLogout, - }) + it('should render OpentronsAI', () => { render() - const logoutButton = screen.getByText('Logout') - fireEvent.click(logoutButton) - expect(mockLogout).toHaveBeenCalled() + expect(screen.getByText('mock OpentronsAI')).toBeInTheDocument() }) }) diff --git a/opentrons-ai-client/src/App.tsx b/opentrons-ai-client/src/App.tsx index 263ea02c844..104977150fc 100644 --- a/opentrons-ai-client/src/App.tsx +++ b/opentrons-ai-client/src/App.tsx @@ -1,82 +1,5 @@ -import { useEffect } from 'react' -import { useAuth0 } from '@auth0/auth0-react' -import { useTranslation } from 'react-i18next' -import { useForm, FormProvider } from 'react-hook-form' -import { useAtom } from 'jotai' -import { - COLORS, - Flex, - Link as LinkButton, - POSITION_ABSOLUTE, - POSITION_RELATIVE, - TYPOGRAPHY, -} from '@opentrons/components' - -import { tokenAtom } from './resources/atoms' -import { useGetAccessToken } from './resources/hooks' -import { SidePanel } from './molecules/SidePanel' -import { Loading } from './molecules/Loading' -import { MainContentContainer } from './organisms/MainContentContainer' - -export interface InputType { - userPrompt: string -} +import { OpentronsAI } from './OpentronsAI' export function App(): JSX.Element | null { - const { t } = useTranslation('protocol_generator') - const { isAuthenticated, logout, isLoading, loginWithRedirect } = useAuth0() - const [, setToken] = useAtom(tokenAtom) - const { getAccessToken } = useGetAccessToken() - - const fetchAccessToken = async (): Promise => { - try { - const accessToken = await getAccessToken() - setToken(accessToken) - } catch (error) { - console.error('Error fetching access token:', error) - } - } - const methods = useForm({ - defaultValues: { - userPrompt: '', - }, - }) - - useEffect(() => { - if (!isAuthenticated && !isLoading) { - void loginWithRedirect() - } - if (isAuthenticated) { - void fetchAccessToken() - } - }, [isAuthenticated, isLoading, loginWithRedirect]) - - if (isLoading) { - return - } - - if (!isAuthenticated) { - return null - } - - return ( - - - logout()} - textDecoration={TYPOGRAPHY.textDecorationUnderline} - > - {t('logout')} - - - - - - - - ) + return } diff --git a/opentrons-ai-client/src/OpentronsAI.test.tsx b/opentrons-ai-client/src/OpentronsAI.test.tsx new file mode 100644 index 00000000000..68d604edf07 --- /dev/null +++ b/opentrons-ai-client/src/OpentronsAI.test.tsx @@ -0,0 +1,82 @@ +import { screen } from '@testing-library/react' +import { describe, it, vi, beforeEach } from 'vitest' +import * as auth0 from '@auth0/auth0-react' + +import { renderWithProviders } from './__testing-utils__' +import { i18n } from './i18n' +import { Loading } from './molecules/Loading' + +import { OpentronsAI } from './OpentronsAI' +import { Landing } from './pages/Landing' +import { useGetAccessToken } from './resources/hooks' +import { Header } from './molecules/Header' +import { Footer } from './molecules/Footer' + +vi.mock('@auth0/auth0-react') + +vi.mock('./pages/Landing') +vi.mock('./molecules/Header') +vi.mock('./molecules/Footer') +vi.mock('./molecules/Loading') +vi.mock('./resources/hooks/useGetAccessToken') +vi.mock('./analytics/mixpanel') + +const mockUseTrackEvent = vi.fn() + +vi.mock('./resources/hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('OpentronsAI', () => { + beforeEach(() => { + vi.mocked(useGetAccessToken).mockReturnValue({ + getAccessToken: vi.fn().mockResolvedValue('mock access token'), + }) + vi.mocked(Landing).mockReturnValue(
mock Landing page
) + vi.mocked(Loading).mockReturnValue(
mock Loading
) + vi.mocked(Header).mockReturnValue(
mock Header component
) + vi.mocked(Footer).mockReturnValue(
mock Footer component
) + }) + + it('should render loading screen when isLoading is true', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: false, + isLoading: true, + }) + render() + screen.getByText('mock Loading') + }) + + it('should render text', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + }) + render() + screen.getByText('mock Landing page') + }) + + it('should render Header component', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + }) + render() + screen.getByText('mock Header component') + }) + + it('should render Footer component', () => { + ;(auth0 as any).useAuth0 = vi.fn().mockReturnValue({ + isAuthenticated: true, + isLoading: false, + }) + render() + screen.getByText('mock Footer component') + }) +}) diff --git a/opentrons-ai-client/src/OpentronsAI.tsx b/opentrons-ai-client/src/OpentronsAI.tsx new file mode 100644 index 00000000000..621c2453e50 --- /dev/null +++ b/opentrons-ai-client/src/OpentronsAI.tsx @@ -0,0 +1,90 @@ +import { HashRouter } from 'react-router-dom' +import { + DIRECTION_COLUMN, + Flex, + OVERFLOW_AUTO, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { OpentronsAIRoutes } from './OpentronsAIRoutes' +import { useAuth0 } from '@auth0/auth0-react' +import { useAtom } from 'jotai' +import { useEffect } from 'react' +import { Loading } from './molecules/Loading' +import { mixpanelAtom, tokenAtom } from './resources/atoms' +import { useGetAccessToken } from './resources/hooks' +import { initializeMixpanel } from './analytics/mixpanel' +import { useTrackEvent } from './resources/hooks/useTrackEvent' +import { Header } from './molecules/Header' +import { CLIENT_MAX_WIDTH } from './resources/constants' +import { Footer } from './molecules/Footer' + +export function OpentronsAI(): JSX.Element | null { + const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0() + const [, setToken] = useAtom(tokenAtom) + const [mixpanel] = useAtom(mixpanelAtom) + const { getAccessToken } = useGetAccessToken() + const trackEvent = useTrackEvent() + + initializeMixpanel(mixpanel) + + const fetchAccessToken = async (): Promise => { + try { + const accessToken = await getAccessToken() + setToken(accessToken) + } catch (error) { + console.error('Error fetching access token:', error) + } + } + + useEffect(() => { + if (!isAuthenticated && !isLoading) { + void loginWithRedirect() + } + if (isAuthenticated) { + void fetchAccessToken() + } + }, [isAuthenticated, isLoading, loginWithRedirect]) + + useEffect(() => { + if (isAuthenticated) { + trackEvent({ name: 'user-login', properties: {} }) + } + }, [isAuthenticated]) + + if (isLoading) { + return + } + + if (!isAuthenticated) { + return null + } + + return ( +
+ +
+ + + + + + + +
+ +
+ ) +} diff --git a/opentrons-ai-client/src/OpentronsAIRoutes.tsx b/opentrons-ai-client/src/OpentronsAIRoutes.tsx new file mode 100644 index 00000000000..630429c2aa1 --- /dev/null +++ b/opentrons-ai-client/src/OpentronsAIRoutes.tsx @@ -0,0 +1,39 @@ +import { Route, Navigate, Routes } from 'react-router-dom' +import { Landing } from './pages/Landing' + +import type { RouteProps } from './resources/types' + +const opentronsAIRoutes: RouteProps[] = [ + // replace Landing with the correct component + { + Component: Landing, + name: 'Create A New Protocol', + navLinkTo: '/new-protocol', + path: '/new-protocol', + }, + { + Component: Landing, + name: 'Update An Existing Protocol', + navLinkTo: '/update-protocol', + path: '/update-protocol', + }, +] + +export function OpentronsAIRoutes(): JSX.Element { + const landingPage: RouteProps = { + Component: Landing, + name: 'Landing', + navLinkTo: '/', + path: '/', + } + const allRoutes: RouteProps[] = [...opentronsAIRoutes, landingPage] + + return ( + + {allRoutes.map(({ Component, path }: RouteProps) => ( + } /> + ))} + } /> + + ) +} diff --git a/opentrons-ai-client/src/analytics/mixpanel.ts b/opentrons-ai-client/src/analytics/mixpanel.ts new file mode 100644 index 00000000000..eb81b72e6e3 --- /dev/null +++ b/opentrons-ai-client/src/analytics/mixpanel.ts @@ -0,0 +1,67 @@ +import mixpanel from 'mixpanel-browser' +import { getHasOptedIn } from './selectors' + +export const getIsProduction = (): boolean => + global.location.host === 'designer.opentrons.com' // UPDATE THIS TO CORRECT URL + +export type AnalyticsEvent = + | { + name: string + properties: Record + superProperties?: Record + } + | { superProperties: Record } + +// pulled in from environment at build time +const MIXPANEL_ID = process.env.OT_AI_CLIENT_MIXPANEL_ID + +const MIXPANEL_OPTS = { + // opt out by default + opt_out_tracking_by_default: true, +} + +export function initializeMixpanel(state: any): void { + const optedIn = getHasOptedIn(state) ?? false + if (MIXPANEL_ID != null) { + console.debug('Initializing Mixpanel', { optedIn }) + + mixpanel.init(MIXPANEL_ID, MIXPANEL_OPTS) + setMixpanelTracking(optedIn) + trackEvent({ name: 'appOpen', properties: {} }, optedIn) // TODO IMMEDIATELY: do we want this? + } else { + console.warn('MIXPANEL_ID not found; this is a bug if build is production') + } +} + +export function trackEvent(event: AnalyticsEvent, optedIn: boolean): void { + console.debug('Trackable event', { event, optedIn }) + if (MIXPANEL_ID != null && optedIn) { + if ('superProperties' in event && event.superProperties != null) { + mixpanel.register(event.superProperties) + } + if ('name' in event && event.name != null) { + mixpanel.track(event.name, event.properties) + } + } +} + +export function setMixpanelTracking(optedIn: boolean): void { + if (MIXPANEL_ID != null) { + if (optedIn) { + console.debug('User has opted into analytics; tracking with Mixpanel') + mixpanel.opt_in_tracking() + // Register "super properties" which are included with all events + mixpanel.register({ + appVersion: 'test', // TODO update this? + // NOTE(IL, 2020): Since PD may be in the same Mixpanel project as other OT web apps, this 'appName' property is intended to distinguish it + appName: 'opentronsAIClient', + }) + } else { + console.debug( + 'User has opted out of analytics; stopping Mixpanel tracking' + ) + mixpanel.opt_out_tracking() + mixpanel.reset() + } + } +} diff --git a/opentrons-ai-client/src/analytics/selectors.ts b/opentrons-ai-client/src/analytics/selectors.ts new file mode 100644 index 00000000000..b55165f3049 --- /dev/null +++ b/opentrons-ai-client/src/analytics/selectors.ts @@ -0,0 +1,2 @@ +export const getHasOptedIn = (state: any): boolean | null => + state.analytics.hasOptedIn diff --git a/opentrons-ai-client/src/assets/images/welcome_dashboard.png b/opentrons-ai-client/src/assets/images/welcome_dashboard.png new file mode 100644 index 00000000000..c6f84b4429b Binary files /dev/null and b/opentrons-ai-client/src/assets/images/welcome_dashboard.png differ diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index 21db34105db..bffc4a9265a 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -3,6 +3,7 @@ "api": "API: An API level is 2.15", "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters (uL) and giving exact source and destination locations.", + "copyright": "Copyright © 2024 Opentrons", "copy_code": "Copy code", "choose_file": "Choose file", "disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.", @@ -12,6 +13,12 @@ "got_feedback": "Got feedback? We love to hear it.", "key_info": "Here are some key pieces of information to provide in your prompt:", "labware_and_tipracks": "Labware and tip racks: Use names from the Opentrons Labware Library.", + "landing_page_body": "Get started building a prompt that will generate a Python protocol that you can use on your Opentrons robot. OpentronsAI lets you create and optimize your protocol by responding in natural language.", + "landing_page_body_mobile": "Use a desktop browser to use OpentronsAI.", + "landing_page_button_new_protocol": "Create a new protocol", + "landing_page_button_update_protocol": "Update an existing protocol", + "landing_page_heading": "Welcome to OpentronsAI", + "landing_page_image_alt": "welcome image", "liquid_locations": "Liquid locations: Describe where liquids should go in the labware.", "loading": "Loading...", "login": "Login", @@ -25,6 +32,7 @@ "pcr_flex": "PCR (Flex)", "pcr": "PCR", "pipettes": "Pipettes: Specify your pipettes, including the volume, number of channels, and whether they’re mounted on the left or right.", + "privacy_policy": "By continuing, you agree to the Opentrons Privacy Policy and End user license agreement", "protocol_file": "Protocol file", "provide_details_of_changes": "Provide details of changes you want to make", "python_file_type_error": "Python file type required", diff --git a/opentrons-ai-client/src/molecules/Footer/Footer.stories.tsx b/opentrons-ai-client/src/molecules/Footer/Footer.stories.tsx new file mode 100644 index 00000000000..78fdf2d42bc --- /dev/null +++ b/opentrons-ai-client/src/molecules/Footer/Footer.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Footer } from '.' +import { COLORS, Flex } from '@opentrons/components' + +const meta: Meta = { + title: 'AI/Molecules/Footer', + component: Footer, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta + +type Story = StoryObj + +export const FooterExample: Story = {} diff --git a/opentrons-ai-client/src/molecules/Footer/__tests__/Footer.test.tsx b/opentrons-ai-client/src/molecules/Footer/__tests__/Footer.test.tsx new file mode 100644 index 00000000000..704855096ed --- /dev/null +++ b/opentrons-ai-client/src/molecules/Footer/__tests__/Footer.test.tsx @@ -0,0 +1,38 @@ +import { Footer } from '..' +import { renderWithProviders } from '../../../__testing-utils__' +import { screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { i18n } from '../../../i18n' + +const render = (): ReturnType => { + return renderWithProviders(