From 56679aa6872068f262fe0d0d4e1d525038a56cd9 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 27 May 2026 12:59:25 -0400 Subject: [PATCH 1/6] fix: refactor custom input labels to simplify --- packages/blockly/core/block_aria_composer.ts | 108 +++++++++++-------- packages/blockly/core/block_svg.ts | 4 +- packages/blockly/core/inputs/input.ts | 15 ++- packages/blockly/core/rendered_connection.ts | 3 - packages/blockly/core/shortcut_items.ts | 14 +-- packages/blockly/tests/mocha/aria_test.js | 56 ++++++++++ 6 files changed, 134 insertions(+), 66 deletions(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 4bebba9975c..9fdcd7051d0 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -50,18 +50,18 @@ export enum ConnectionPreposition { * The returned label will be specialized based on whether the block is part of a * flyout. * + * Custom input labels (from {@link Input.setAriaLabelProvider}) are not included + * here; they are used only in move-mode disambiguation and parent-input context + * via {@link Input.getAriaLabelText}. + * * @internal * @param block The block for which an ARIA representation should be created. * @param verbosity How much detail to include in the description. - * @param useCustomInputLabels Whether to use custom labels for inputs, if they - * exist. We don't want to do this when just reading a block's label, but do - * want to in other scenarios such as move mode. * @returns The ARIA representation for the specified block. */ export function computeAriaLabel( block: BlockSvg, verbosity = Verbosity.STANDARD, - useCustomInputLabels = true, ) { if (block.isSimpleReporter()) { // special case for full-block field blocks. @@ -73,7 +73,7 @@ export function computeAriaLabel( return [ verbosity >= Verbosity.STANDARD && getBeginStackLabel(block), getParentInputLabel(block), - ...getInputLabels(block, verbosity, useCustomInputLabels), + ...getInputLabels(block, verbosity), verbosity === Verbosity.LOQUACIOUS && getParentToolboxCategoryLabel(block), verbosity >= Verbosity.STANDARD && getDisabledLabel(block), verbosity >= Verbosity.STANDARD && getCollapsedLabel(block), @@ -272,21 +272,18 @@ function getBeginStackLabel(block: BlockSvg) { * their contents are returned as a single item in the array per top-level * input. * - * Generally, if a custom label for an input is provided, that is preferred. - * However, we do not surface the custom labels when simply reading the text of - * the block. They are used as supplementary information for situations like - * move mode or when an input itself is focused. + * Uses derived labels only (field row text and connected block content via + * {@link Input.getLabel}). Custom input labels are not included; see + * {@link Input.getAriaLabelText} for move-mode and parent-input usage. * * @internal * @param block The block to retrieve a list of field/input labels for. - * @param verbosity - * @param useCustomLabels whether to use the custom label for an input, if it's present. + * @param verbosity How much detail to include in each input label. * @returns A list of field/input labels for the given block. */ export function getInputLabels( block: BlockSvg, verbosity = Verbosity.STANDARD, - useCustomLabels = true, ): string[] { const visibleInputs = block.inputList.filter((input) => input.isVisible()); let inputsToLabel = visibleInputs; @@ -306,15 +303,13 @@ export function getInputLabels( } } - return inputsToLabel.map((input) => { - const customLabel = useCustomLabels ? input.getAriaLabelText() : null; - return customLabel ?? input.getLabel(verbosity, useCustomLabels); - }); + return inputsToLabel.map((input) => input.getLabel(verbosity)); } /** - * Returns a subset of labels for inputs on the given block, ending at the - * specified input. + * Returns a subset of derived labels for inputs on the given block, ending at + * the specified input. Used to disambiguate move targets and connection + * highlights when no custom label is set. * * The subset is determined based on the input type: * - For non-statement inputs, only the label for the given input is returned. @@ -323,41 +318,66 @@ export function getInputLabels( * begins immediately after the previous statement input, or at the start of * the block if none exists. * + * Label resolution (see also {@link computeMoveConnectionLabel}): + * 1. Custom labels ({@link Input.getAriaLabelText}) are handled by callers, not here. + * 2. Derived labels from {@link Input.getLabel} (field row + child blocks). + * 3. Numbered fallback ({@link Msg.INPUT_LABEL_INDEX}) when tier 2 is empty. + * For the statement target input, the fallback is omitted if any earlier + * input in the subset already produced a label. + * * @internal * @param block The block to retrieve a list of field/input labels for. * @param input The input that defines the end of the subset. * @returns A list of field/input labels for the given block. */ -export function getInputLabelsSubset( - block: BlockSvg, - input: Input, - includeFallbackLabels = true, -): string[] { +export function getInputLabelsSubset(block: BlockSvg, input: Input): string[] { const inputIndex = block.inputList.indexOf(input); if (inputIndex === -1) { throw new Error( `Input with name "${input.name}" not found on block with id "${block.id}".`, ); } + const isStatementTarget = input.type === inputTypes.STATEMENT; - const startIndex = - input.type === inputTypes.STATEMENT - ? findStartOfStatementSection(block.inputList, inputIndex) - : inputIndex; + const startIndex = isStatementTarget + ? findStartOfStatementSection(block.inputList, inputIndex) + : inputIndex; - return block.inputList + // For statement inputs, we include all visible inputs from the start + // of the current statement section up to and including the target input. + // For non-statement inputs, this will just be the target input itself. + const inputsInSubset = block.inputList .slice(startIndex, inputIndex + 1) - .filter((input) => input.isVisible()) - .map( - (input) => - input.getLabel(Verbosity.TERSE, false) || - (includeFallbackLabels - ? Msg['INPUT_LABEL_INDEX'].replace( - '%1', - (input.getIndex() + 1).toString(), - ) - : undefined), - ) + .filter((subsetInput) => subsetInput.isVisible()); + + // The derived labels are based on the field row and any connected child + // blocks. + const derivedLabels = inputsInSubset.map((subsetInput) => + subsetInput.getLabel(Verbosity.TERSE), + ); + + // For statement inputs, we only include the fallback label ("input %1") + // for the target input if no preceding input in the subset has a label. + // This prevents, e.g., "else" statement inputs from being read as "else, input 2". + const precedingLabelsProvideContext = + isStatementTarget && derivedLabels.slice(0, -1).some((label) => !!label); + + return derivedLabels + .map((label, index) => { + if (label) { + return label; + } + const subsetInput = inputsInSubset[index]; + const isStatementTargetInput = + isStatementTarget && index === derivedLabels.length - 1; + if (isStatementTargetInput && precedingLabelsProvideContext) { + return undefined; + } + return Msg['INPUT_LABEL_INDEX'].replace( + '%1', + (subsetInput.getIndex() + 1).toString(), + ); + }) .filter((label) => label !== undefined); } @@ -468,7 +488,13 @@ function getAnnouncementTemplate(preposition: ConnectionPreposition): string { } /** - * Returns a label for a connection includes either a block label, input label or both. + * Returns a label for a connection that includes either a block label, input + * label, or both. + * + * Input label resolution: + * 1. Custom label from {@link Input.getAriaLabelText} when set. + * 2. Otherwise derived labels from {@link getInputLabelsSubset} (field row, + * child blocks, and numbered fallbacks as needed). * * @param conn The connection to generate a label for. * @param baseLabel An optional block label to include in the returned string. @@ -483,8 +509,6 @@ function computeMoveConnectionLabel( let inputLabel = input.getAriaLabelText(); - // If the input doesn't have a custom ARIA label, compute one using the labels from - // nearby fields. if (!inputLabel) { const labels = getInputLabelsSubset(conn.getSourceBlock(), input); if (!labels.length) return baseLabel; diff --git a/packages/blockly/core/block_svg.ts b/packages/blockly/core/block_svg.ts index 94e792fa87c..2a4b7568a4d 100644 --- a/packages/blockly/core/block_svg.ts +++ b/packages/blockly/core/block_svg.ts @@ -2034,7 +2034,7 @@ export class BlockSvg * @returns An accessibility description of this block. */ getAriaLabel(verbosity: aria.Verbosity) { - return computeAriaLabel(this, verbosity, false); + return computeAriaLabel(this, verbosity); } /** @@ -2051,7 +2051,7 @@ export class BlockSvg block = block.getNextBlock(); } if (count <= 1) { - return computeAriaLabel(this, aria.Verbosity.TERSE, false); + return computeAriaLabel(this, aria.Verbosity.TERSE); } const labelTemplate = Msg['BLOCK_LABEL_STACK_BLOCKS']; diff --git a/packages/blockly/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts index b00fb82d99d..31b51a1c32d 100644 --- a/packages/blockly/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -399,13 +399,14 @@ export class Input { } /** - * Returns an accessibility label describing this input, including the labels - * of any fields on the input and the labels of any connected blocks, to help - * disambiguate this input from others on the same block. + * Returns a derived accessibility label for this input: field row text plus + * labels of any connected child blocks. Does not include custom labels from + * {@link getAriaLabelText}; those are used in move-mode and parent-input + * context only. * * @internal */ - getLabel(verbosity = Verbosity.STANDARD, useCustomLabels = true): string { + getLabel(verbosity = Verbosity.STANDARD): string { if (!this.isVisible()) return ''; const labels = computeFieldRowLabel(this, false, verbosity); @@ -414,11 +415,7 @@ export class Input { const childBlock = this.connection.targetBlock(); if (childBlock && !childBlock.isInsertionMarker()) { labels.push( - getInputLabels( - childBlock as BlockSvg, - verbosity, - useCustomLabels, - ).join(', '), + getInputLabels(childBlock as BlockSvg, verbosity).join(', '), ); } } diff --git a/packages/blockly/core/rendered_connection.ts b/packages/blockly/core/rendered_connection.ts index 735d403b015..faa448d3a10 100644 --- a/packages/blockly/core/rendered_connection.ts +++ b/packages/blockly/core/rendered_connection.ts @@ -355,14 +355,11 @@ export class RenderedConnection // Use the custom label for an input if it exists, otherwise use the // "field row" approach to get the default label for the input. - // Don't include the "input 1" fallback for default labels, since - // the input is already being described as a statement or value input. const parentInputLabel = parentInput?.getAriaLabelText() ?? getInputLabelsSubset( parentInput.getSourceBlock() as BlockSvg, parentInput, - false, ).join(', '); if (this.type === ConnectionType.NEXT_STATEMENT) { aria.setState( diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts index ef6b4fd3c8a..9775ec86dc9 100644 --- a/packages/blockly/core/shortcut_items.ts +++ b/packages/blockly/core/shortcut_items.ts @@ -807,11 +807,7 @@ export function registerFocusToolbox() { */ export function registerReadInformation() { const announceBlockInformation = (block: BlockSvg) => { - const description = computeAriaLabel( - block, - aria.Verbosity.LOQUACIOUS, - false, - ); + const description = computeAriaLabel(block, aria.Verbosity.LOQUACIOUS); aria.announceDynamicAriaState(description); }; @@ -898,14 +894,12 @@ export function registerReadExtendedInformation() { } if (startBlock !== block) { - toAnnounce.push( - computeAriaLabel(startBlock, aria.Verbosity.TERSE, false), - ); + toAnnounce.push(computeAriaLabel(startBlock, aria.Verbosity.TERSE)); } let parent = startBlock.getParent(); while (parent) { - toAnnounce.push(computeAriaLabel(parent, aria.Verbosity.TERSE, false)); + toAnnounce.push(computeAriaLabel(parent, aria.Verbosity.TERSE)); parent = parent.getParent(); } @@ -917,7 +911,7 @@ export function registerReadExtendedInformation() { toAnnounce.push( Msg['CURRENT_BLOCK_ANNOUNCEMENT'].replace( '%1', - computeAriaLabel(block, aria.Verbosity.TERSE, false), + computeAriaLabel(block, aria.Verbosity.TERSE), ), ); } diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index ab03593f221..044bf05ceb0 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -563,6 +563,62 @@ suite('ARIA', function () { }); }); + suite('getInputLabelsSubset', function () { + setup(function () { + Blockly.Blocks['aria_subset_test'] = { + init: function () { + this.appendValueInput('IF').appendField('if'); + this.appendStatementInput('DO').appendField('do'); + this.appendDummyInput('DUMMY').appendField("here's a label"); + this.appendEndRowInput('END_ROW').appendField( + new Blockly.FieldImage( + 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 15, + 15, + {alt: '*', flipRtl: false}, + ), + ); + this.appendStatementInput('BODY'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + }, + }; + Blockly.Blocks['aria_subset_lone_statement'] = { + init: function () { + this.appendStatementInput('FIRST').appendField('first'); + this.appendStatementInput('SECOND'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + }, + }; + this.renderBlock = (blockType) => { + const block = this.workspace.newBlock(blockType); + block.initSvg(); + block.render(); + return block; + }; + }); + + test('unlabeled statement input omits numbered fallback when section has other labels', function () { + const block = this.renderBlock('aria_subset_test'); + const bodyInput = block.getInput('BODY'); + const labels = getInputLabelsSubset(block, bodyInput); + assert.deepEqual(labels, ["here's a label", '*']); + for (const label of labels) { + assert.notInclude(label, 'input'); + } + }); + + test('unlabeled statement input uses numbered fallback when section has no other labels', function () { + const block = this.renderBlock('aria_subset_lone_statement'); + const secondInput = block.getInput('SECOND'); + const labels = getInputLabelsSubset(block, secondInput); + assert.deepEqual(labels, [ + Blockly.Msg.INPUT_LABEL_INDEX.replace('%1', '2'), + ]); + }); + }); + suite('Rendered connection highlight ARIA', function () { function assertHighlightAria( connection, From 4e274f26d06710e69649d028f63dbbb2f935386d Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 27 May 2026 13:21:29 -0400 Subject: [PATCH 2/6] fix: handle bad field image config from block factory --- packages/blockly/core/field_image.ts | 27 ++++++++++++++++++- .../demos/blockfactory/factory_utils.js | 2 +- .../blockly/tests/mocha/field_image_test.js | 8 ++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/blockly/core/field_image.ts b/packages/blockly/core/field_image.ts index 4ed467b0ed9..b65ff3bc8e9 100644 --- a/packages/blockly/core/field_image.ts +++ b/packages/blockly/core/field_image.ts @@ -113,9 +113,18 @@ export class FieldImage extends Field { if (config) { this.configure_(config); + } else if (isFieldImageConfig(alt)) { + // Block Factory and some hand-written blocks pass a config object as the + // fourth argument instead of using the seventh `config` parameter. + // This is wrong, and typescript will complain about it, but handle it + // for backwards compatibility. + this.configure_(alt); } else { this.flipRtl = !!flipRtl; - this.altText = parsing.replaceMessageReferences(alt) || ''; + this.altText = + typeof alt === 'string' + ? parsing.replaceMessageReferences(alt) || '' + : ''; } this.setValue(parsing.replaceMessageReferences(src)); } @@ -372,6 +381,22 @@ export class FieldImage extends Field { } } +/** + * Returns whether a value is a FieldImage config object passed in place of alt + * text (e.g. `{alt: '*', flipRtl: false}`). You shouldn't do this on purpose, + * but the block factory generates block definitions in this format. + * + * @param value The value to test. + */ +function isFieldImageConfig(value: unknown): value is FieldImageConfig { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + ('alt' in value || 'flipRtl' in value) + ); +} + fieldRegistry.register('field_image', FieldImage); FieldImage.prototype.DEFAULT_VALUE = ''; diff --git a/packages/blockly/demos/blockfactory/factory_utils.js b/packages/blockly/demos/blockfactory/factory_utils.js index 2d51b4c6096..a65b6ff2172 100644 --- a/packages/blockly/demos/blockfactory/factory_utils.js +++ b/packages/blockly/demos/blockfactory/factory_utils.js @@ -458,7 +458,7 @@ FactoryUtils.getFieldsJs_ = function(block) { } break; case 'field_image': - // Result: new Blockly.FieldImage('http://...', 80, 60, '*') + // Result: new Blockly.FieldImage('http://...', 80, 60, {alt: '*', flipRtl: false}) var src = JSON.stringify(block.getFieldValue('SRC')); var width = Number(block.getFieldValue('WIDTH')); var height = Number(block.getFieldValue('HEIGHT')); diff --git a/packages/blockly/tests/mocha/field_image_test.js b/packages/blockly/tests/mocha/field_image_test.js index 2c317f21852..d8d8d7e7c9b 100644 --- a/packages/blockly/tests/mocha/field_image_test.js +++ b/packages/blockly/tests/mocha/field_image_test.js @@ -186,6 +186,14 @@ suite('Image Fields', function () { }); assert.equal(field.getText(), 'alt'); }); + test('JS Configuration - Block Factory style (config as 4th arg)', function () { + const field = new Blockly.FieldImage('src', 10, 10, { + alt: 'alt', + flipRtl: false, + }); + assert.equal(field.getText(), 'alt'); + assert.isFalse(field.getFlipRtl()); + }); test('JS Configuration - Ignore', function () { const field = new Blockly.FieldImage('src', 10, 10, 'alt', null, null, { alt: 'configAlt', From 9b37c1d60e7099501fbd255e3661252c8af69698 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 27 May 2026 14:45:06 -0400 Subject: [PATCH 3/6] chore: remove stray log --- packages/blockly/core/dragging/block_drag_strategy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index df309d5c854..2a2bd0abab6 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -1277,7 +1277,6 @@ export class BlockDragStrategy implements IDragStrategy { * @param block The block to re-disable, if applicable. */ private redisableAllDraggedBlocks(block: BlockSvg) { - console.log('redisable'); const oldUndo = eventUtils.getRecordUndo(); eventUtils.setRecordUndo(false); block.getDescendants(false).forEach((descendant) => { From 01ace79033af12c57a9dad69e34c8101e5b2f771 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 27 May 2026 16:01:15 -0400 Subject: [PATCH 4/6] fix: fix move mode labels --- packages/blockly/core/block_aria_composer.ts | 17 +++++++++--- packages/blockly/tests/mocha/aria_test.js | 29 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 9fdcd7051d0..74be00a213e 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -197,6 +197,10 @@ export function computeFieldRowLabel( * statement input of the parent block (in this case, the label * would be redundant with the parent block's label) * + * For statement inputs without their own field labels, labels from other + * inputs in the same statement section are included (via + * {@link getInputLabelsSubset}), consistent with move-target disambiguation. + * * For statement inputs, the resolved label (whether custom or fallback) is * wrapped in the "Begin %1" prefix so the readout indicates that the child * block starts the body of the statement input. @@ -235,7 +239,14 @@ function getParentInputLabel(block: BlockSvg) { return undefined; } - inputLabel = computeFieldRowLabel(parentInput, true); + const sectionLabels = getInputLabelsSubset( + parentBlock as BlockSvg, + parentInput, + ); + if (!sectionLabels.length) { + return undefined; + } + inputLabel = sectionLabels.join(', '); } if (parentInput.type === inputTypes.STATEMENT) { @@ -368,9 +379,7 @@ export function getInputLabelsSubset(block: BlockSvg, input: Input): string[] { return label; } const subsetInput = inputsInSubset[index]; - const isStatementTargetInput = - isStatementTarget && index === derivedLabels.length - 1; - if (isStatementTargetInput && precedingLabelsProvideContext) { + if (precedingLabelsProvideContext) { return undefined; } return Msg['INPUT_LABEL_INDEX'].replace( diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index 044bf05ceb0..4810fa80acb 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -396,6 +396,35 @@ suite('ARIA', function () { assert.isTrue(label.startsWith('Begin else')); }); + test('Statement child includes labels from other inputs in the same statement section', function () { + Blockly.Blocks['aria_parent_label_test'] = { + init: function () { + this.appendValueInput('IF').appendField('if'); + this.appendStatementInput('DO').appendField('do'); + this.appendDummyInput('DUMMY').appendField("here's a label"); + this.appendEndRowInput('END_ROW').appendField( + new Blockly.FieldImage( + 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 15, + 15, + {alt: '*', flipRtl: false}, + ), + ); + this.appendStatementInput('BODY'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + }, + }; + const block = this.makeBlock('aria_parent_label_test'); + const printBlock = this.makeBlock('text_print'); + block.getInput('BODY').connection.connect(printBlock.previousConnection); + const label = Blockly.utils.aria.getState( + printBlock.getFocusableElement(), + Blockly.utils.aria.State.LABEL, + ); + assert.isTrue(label.startsWith("Begin here's a label, *")); + }); + test('A custom statement input label is wrapped in the "Begin" prefix', function () { const ifBlock = this.makeBlock('controls_ifelse'); ifBlock.getInput('ELSE').setAriaLabelProvider('otherwise do'); From 8fe097d6a93287ade537bfff6c5dad06c0cda250 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 27 May 2026 16:23:25 -0400 Subject: [PATCH 5/6] fix: dont use numbered inputs for dummy and end row inputs --- packages/blockly/core/block_aria_composer.ts | 12 +++++++- packages/blockly/tests/mocha/aria_test.js | 29 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/blockly/core/block_aria_composer.ts b/packages/blockly/core/block_aria_composer.ts index 74be00a213e..72ef21826eb 100644 --- a/packages/blockly/core/block_aria_composer.ts +++ b/packages/blockly/core/block_aria_composer.ts @@ -379,7 +379,17 @@ export function getInputLabelsSubset(block: BlockSvg, input: Input): string[] { return label; } const subsetInput = inputsInSubset[index]; - if (precedingLabelsProvideContext) { + // Dummy and end-row inputs are not connection inputs; getIndex() is -1 + // and would produce a misleading "input 0" fallback label. + if ( + subsetInput.type === inputTypes.DUMMY || + subsetInput.type === inputTypes.END_ROW + ) { + return undefined; + } + const isStatementTargetInput = + isStatementTarget && index === derivedLabels.length - 1; + if (isStatementTargetInput && precedingLabelsProvideContext) { return undefined; } return Msg['INPUT_LABEL_INDEX'].replace( diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index 4810fa80acb..ab640e9988f 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -646,6 +646,35 @@ suite('ARIA', function () { Blockly.Msg.INPUT_LABEL_INDEX.replace('%1', '2'), ]); }); + + test('dummy inputs in a statement section do not produce input 0 fallback', function () { + Blockly.Blocks['makecode_if_else'] = { + init: function () { + this.appendValueInput('IF0').appendField('if'); + this.appendDummyInput('THEN0').appendField('then'); + this.appendStatementInput('DO0'); + this.appendDummyInput('ELSETITLE').appendField('else'); + this.appendDummyInput('ELSEBUTTONS').appendField( + new Blockly.FieldImage( + 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 24, + 24, + {alt: '*', flipRtl: false}, + ), + ); + this.appendStatementInput('ELSE'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + }, + }; + const block = this.renderBlock('makecode_if_else'); + const elseInput = block.getInput('ELSE'); + const labels = getInputLabelsSubset(block, elseInput); + assert.deepEqual(labels, ['else']); + for (const label of labels) { + assert.notInclude(label, 'input 0'); + } + }); }); suite('Rendered connection highlight ARIA', function () { From 63a6084b8a098c5c5460da215545626892a32be5 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 28 May 2026 09:21:54 -0400 Subject: [PATCH 6/6] chore: fix test --- packages/blockly/tests/mocha/aria_test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index ab640e9988f..54d5b738905 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -670,10 +670,8 @@ suite('ARIA', function () { const block = this.renderBlock('makecode_if_else'); const elseInput = block.getInput('ELSE'); const labels = getInputLabelsSubset(block, elseInput); - assert.deepEqual(labels, ['else']); - for (const label of labels) { - assert.notInclude(label, 'input 0'); - } + assert.include(labels, 'else'); + assert.notInclude(labels.join(', '), 'input 0'); }); });