From 2375f81e206e2ee33153b6f004cc9198f70df6d3 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Wed, 8 Jul 2026 09:19:13 -0300 Subject: [PATCH 1/3] updated integration with upcoming block accessibility checks v4 --- bibliography-builder.php | 82 +++++-- package-lock.json | 21 +- src/index.js | 3 - src/lib/block-accessibility-checks.js | 86 +------ src/lib/block-accessibility-checks.test.js | 251 +-------------------- src/validation.js | 37 ++- src/validation.test.js | 116 +++++++++- tests/e2e/a11y.spec.js | 8 +- tests/phpunit/A11yIntegrationTest.php | 143 +++++++++--- 9 files changed, 340 insertions(+), 407 deletions(-) diff --git a/bibliography-builder.php b/bibliography-builder.php index 080ef3f..c482c5b 100644 --- a/bibliography-builder.php +++ b/bibliography-builder.php @@ -1810,8 +1810,8 @@ function bibliography_builder_block_init() { * @param object $registry BAC check registry instance. * @since 1.1.0 */ -function bibliography_builder_register_a11y_checks( $registry ) { - if ( ! is_object( $registry ) || ! method_exists( $registry, 'register_check' ) ) { +function bibliography_builder_register_a11y_checks() { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { return; } @@ -1820,30 +1820,80 @@ function bibliography_builder_register_a11y_checks( $registry ) { 'borges-bibliography-builder' ); - $registry->register_check( + \ba11yc_register_block_check( 'bibliography-builder/bibliography', - 'empty_bibliography', array( - 'error_msg' => __( 'Bibliography block contains no citations.', 'borges-bibliography-builder' ), - 'warning_msg' => __( 'Bibliography block contains no citations.', 'borges-bibliography-builder' ), - 'description' => __( 'Add at least one citation before publishing.', 'borges-bibliography-builder' ), - 'type' => 'error', - 'category' => 'accessibility', + 'namespace' => 'borges-bibliography-builder', + 'name' => 'empty_bibliography', + 'error_msg' => __( 'Bibliography block contains no citations.', 'borges-bibliography-builder' ), + 'warning_msg' => __( 'Bibliography block contains no citations.', 'borges-bibliography-builder' ), + 'description' => __( 'Add at least one citation before publishing.', 'borges-bibliography-builder' ), + 'level' => 'error', + 'configurable' => true, + 'category' => 'accessibility', ) ); - $registry->register_check( + \ba11yc_register_block_check( 'bibliography-builder/bibliography', - 'heading_missing', array( - 'error_msg' => $heading_missing_message, - 'warning_msg' => $heading_missing_message, - 'description' => __( + 'namespace' => 'borges-bibliography-builder', + 'name' => 'heading_missing', + 'error_msg' => $heading_missing_message, + 'warning_msg' => $heading_missing_message, + 'description' => __( 'Add a heading in block settings so the bibliography is announced as a document section.', 'borges-bibliography-builder' ), - 'type' => 'warning', - 'category' => 'accessibility', + 'level' => 'warning', + 'configurable' => true, + 'category' => 'accessibility', + ) + ); + + \ba11yc_register_block_check( + 'bibliography-builder/bibliography', + array( + 'namespace' => 'borges-bibliography-builder', + 'name' => 'raw_url_link_text', + 'error_msg' => __( + 'One or more citations have a URL or DOI as the only identifier. A descriptive title helps screen reader users understand the link destination.', + 'borges-bibliography-builder' + ), + 'warning_msg' => __( + 'One or more citations have a URL or DOI as the only identifier. A descriptive title helps screen reader users understand the link destination.', + 'borges-bibliography-builder' + ), + 'description' => __( + 'Citations with a URL or DOI should have a descriptive title as link text.', + 'borges-bibliography-builder' + ), + 'level' => 'warning', + 'configurable' => true, + 'category' => 'accessibility', + ) + ); + + \ba11yc_register_block_check( + 'bibliography-builder/bibliography', + array( + 'namespace' => 'borges-bibliography-builder', + 'name' => 'all_metadata_disabled', + 'error_msg' => __( + 'All machine-readable metadata outputs (JSON-LD, COinS, CSL-JSON) are disabled. Enabling at least one improves citation-manager interoperability.', + 'borges-bibliography-builder' + ), + 'warning_msg' => __( + 'All machine-readable metadata outputs (JSON-LD, COinS, CSL-JSON) are disabled. Enabling at least one improves citation-manager interoperability.', + 'borges-bibliography-builder' + ), + 'description' => __( + 'Enable at least one metadata output (JSON-LD, COinS, or CSL-JSON) to improve citation-manager interoperability.', + 'borges-bibliography-builder' + ), + 'level' => 'warning', + 'configurable' => true, + 'category' => 'accessibility', ) ); } diff --git a/package-lock.json b/package-lock.json index 813923f..6fb69c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13609,7 +13609,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.2" @@ -16588,7 +16588,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -23195,7 +23195,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -25646,21 +25646,6 @@ "dev": true, "license": "MIT" }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", diff --git a/src/index.js b/src/index.js index 70655f1..50472f8 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,6 @@ import { DEFAULT_CITATION_STYLE, getDefaultHeadingText, } from './lib/formatting'; -import { registerBACChecks } from './lib/block-accessibility-checks'; registerBlockType(metadata.name, { edit: Edit, @@ -26,5 +25,3 @@ registerBlockType(metadata.name, { }, ], }); - -registerBACChecks(); diff --git a/src/lib/block-accessibility-checks.js b/src/lib/block-accessibility-checks.js index b87b5f5..6dd03ea 100644 --- a/src/lib/block-accessibility-checks.js +++ b/src/lib/block-accessibility-checks.js @@ -1,83 +1,3 @@ -const BLOCK_NAME = 'bibliography-builder/bibliography'; - -const RAW_URL_RE = /^https?:\/\/\S+$|^10\.\d{4,}\/\S+$/i; - -function isRawUrlText(text) { - return typeof text === 'string' && RAW_URL_RE.test(text.trim()); -} - -function hasRawUrlLinks(citations) { - if (!Array.isArray(citations)) { - return false; - } - return citations.some((csl) => { - const url = csl?.URL || csl?.DOI; - const title = csl?.title; - if (!url) { - return false; - } - return !title || isRawUrlText(title); - }); -} - -const checks = [ - { - id: 'empty_bibliography', - message: - 'This bibliography block has no citations. Empty blocks are omitted from the published page.', - severity: 'error', - test(attributes) { - const { citations } = attributes; - return !Array.isArray(citations) || citations.length === 0; - }, - }, - { - id: 'heading_missing', - message: - 'This bibliography block has citations but no visible heading or accessible label. Add a heading so readers can identify the reference list.', - severity: 'warning', - test(attributes) { - const { citations, headingText } = attributes; - const hasCitations = - Array.isArray(citations) && citations.length > 0; - const hasHeading = - typeof headingText === 'string' && headingText.trim() !== ''; - return hasCitations && !hasHeading; - }, - }, - { - id: 'raw_url_link_text', - message: - 'One or more citations have a URL or DOI as the only identifier. A descriptive title helps screen reader users understand the link destination.', - severity: 'warning', - test(attributes) { - return hasRawUrlLinks(attributes.citations); - }, - }, - { - id: 'all_metadata_disabled', - message: - 'All machine-readable metadata outputs (JSON-LD, COinS, CSL-JSON) are disabled. Enabling at least one improves citation-manager interoperability.', - severity: 'warning', - test(attributes) { - const { outputJsonLd, outputCoins, outputCslJson } = attributes; - return !outputJsonLd && !outputCoins && !outputCslJson; - }, - }, -]; - -export function registerBACChecks() { - if ( - typeof window === 'undefined' || - typeof window.BlockAccessibilityChecks === 'undefined' - ) { - return; - } - - const { registerBlockChecks } = window.BlockAccessibilityChecks; - if (typeof registerBlockChecks !== 'function') { - return; - } - - registerBlockChecks(BLOCK_NAME, checks); -} +// BAC v3 window-global integration path removed in v4. +// All checks are now registered via ba11yc_register_block_check() in PHP +// and validated via the ba11yc.validateBlock filter in src/validation.js. diff --git a/src/lib/block-accessibility-checks.test.js b/src/lib/block-accessibility-checks.test.js index 8fb8982..6bc62e1 100644 --- a/src/lib/block-accessibility-checks.test.js +++ b/src/lib/block-accessibility-checks.test.js @@ -1,247 +1,8 @@ -import { registerBACChecks } from './block-accessibility-checks'; +// The window.BlockAccessibilityChecks integration path was removed in v4. +// Check logic is now tested via src/validation.test.js. -const BLOCK_NAME = 'bibliography-builder/bibliography'; - -function makeAttributes(overrides = {}) { - return { - citations: [], - headingText: '', - outputJsonLd: true, - outputCoins: false, - outputCslJson: false, - ...overrides, - }; -} - -describe('registerBACChecks', () => { - let originalBAC; - - beforeEach(() => { - originalBAC = window.BlockAccessibilityChecks; - }); - - afterEach(() => { - window.BlockAccessibilityChecks = originalBAC; - }); - - it('is a no-op when window.BlockAccessibilityChecks is absent', () => { - delete window.BlockAccessibilityChecks; - expect(() => registerBACChecks()).not.toThrow(); - }); - - it('is a no-op when registerBlockChecks is not a function', () => { - window.BlockAccessibilityChecks = {}; - expect(() => registerBACChecks()).not.toThrow(); - }); - - it('calls registerBlockChecks with the block name and checks array', () => { - const registerBlockChecks = jest.fn(); - window.BlockAccessibilityChecks = { registerBlockChecks }; - - registerBACChecks(); - - expect(registerBlockChecks).toHaveBeenCalledTimes(1); - const [name, checks] = registerBlockChecks.mock.calls[0]; - expect(name).toBe(BLOCK_NAME); - expect(Array.isArray(checks)).toBe(true); - expect(checks.length).toBeGreaterThan(0); - }); -}); - -describe('BAC check logic', () => { - let checks; - - beforeEach(() => { - const registerBlockChecks = jest.fn(); - window.BlockAccessibilityChecks = { registerBlockChecks }; - registerBACChecks(); - checks = registerBlockChecks.mock.calls[0][1]; - }); - - afterEach(() => { - delete window.BlockAccessibilityChecks; - }); - - function getCheck(id) { - return checks.find((c) => c.id === id); - } - - describe('empty_bibliography', () => { - it('fires when citations is empty', () => { - expect(getCheck('empty_bibliography').test(makeAttributes())).toBe( - true - ); - }); - - it('fires when citations is missing', () => { - expect( - getCheck('empty_bibliography').test( - makeAttributes({ citations: undefined }) - ) - ).toBe(true); - }); - - it('does not fire when citations are present', () => { - expect( - getCheck('empty_bibliography').test( - makeAttributes({ citations: [{ title: 'Test' }] }) - ) - ).toBe(false); - }); - - it('has error severity', () => { - expect(getCheck('empty_bibliography').severity).toBe('error'); - }); - }); - - describe('heading_missing', () => { - it('fires when citations exist but headingText is empty', () => { - expect( - getCheck('heading_missing').test( - makeAttributes({ - citations: [{ title: 'Test' }], - headingText: '', - }) - ) - ).toBe(true); - }); - - it('does not fire when headingText is set', () => { - expect( - getCheck('heading_missing').test( - makeAttributes({ - citations: [{ title: 'Test' }], - headingText: 'References', - }) - ) - ).toBe(false); - }); - - it('does not fire when block is empty', () => { - expect( - getCheck('heading_missing').test( - makeAttributes({ citations: [] }) - ) - ).toBe(false); - }); - - it('has warning severity', () => { - expect(getCheck('heading_missing').severity).toBe('warning'); - }); - }); - - describe('raw_url_link_text', () => { - it('fires when a citation has a URL but no title', () => { - expect( - getCheck('raw_url_link_text').test( - makeAttributes({ - citations: [{ URL: 'https://example.com/paper' }], - }) - ) - ).toBe(true); - }); - - it('fires when a citation has a DOI but no title', () => { - expect( - getCheck('raw_url_link_text').test( - makeAttributes({ - citations: [{ DOI: '10.1234/example' }], - }) - ) - ).toBe(true); - }); - - it('fires when a citation title is itself a raw URL', () => { - expect( - getCheck('raw_url_link_text').test( - makeAttributes({ - citations: [ - { - URL: 'https://example.com', - title: 'https://example.com', - }, - ], - }) - ) - ).toBe(true); - }); - - it('does not fire when citations have descriptive titles', () => { - expect( - getCheck('raw_url_link_text').test( - makeAttributes({ - citations: [ - { URL: 'https://example.com', title: 'My Paper' }, - ], - }) - ) - ).toBe(false); - }); - - it('does not fire when citations array is empty', () => { - expect( - getCheck('raw_url_link_text').test( - makeAttributes({ citations: [] }) - ) - ).toBe(false); - }); - - it('has warning severity', () => { - expect(getCheck('raw_url_link_text').severity).toBe('warning'); - }); - }); - - describe('all_metadata_disabled', () => { - it('fires when all three metadata outputs are disabled', () => { - expect( - getCheck('all_metadata_disabled').test( - makeAttributes({ - outputJsonLd: false, - outputCoins: false, - outputCslJson: false, - }) - ) - ).toBe(true); - }); - - it('does not fire when outputJsonLd is enabled', () => { - expect( - getCheck('all_metadata_disabled').test( - makeAttributes({ - outputJsonLd: true, - outputCoins: false, - outputCslJson: false, - }) - ) - ).toBe(false); - }); - - it('does not fire when outputCoins is enabled', () => { - expect( - getCheck('all_metadata_disabled').test( - makeAttributes({ - outputJsonLd: false, - outputCoins: true, - outputCslJson: false, - }) - ) - ).toBe(false); - }); - - it('does not fire when outputCslJson is enabled', () => { - expect( - getCheck('all_metadata_disabled').test( - makeAttributes({ - outputJsonLd: false, - outputCoins: false, - outputCslJson: true, - }) - ) - ).toBe(false); - }); - - it('has warning severity', () => { - expect(getCheck('all_metadata_disabled').severity).toBe('warning'); - }); - }); +test('BAC v3 window-global path removed', () => { + // All four checks are now registered via ba11yc_register_block_check() in + // PHP and validated via the ba11yc.validateBlock filter in validation.js. + expect(true).toBe(true); }); diff --git a/src/validation.js b/src/validation.js index 5ceeb9b..c7c9302 100644 --- a/src/validation.js +++ b/src/validation.js @@ -7,8 +7,10 @@ * present, and the filter is a no-op if the framework never calls it. * * Checks registered: - * - empty_bibliography (error) — block has no citations - * - heading_missing (warning) — headingText is blank; no heading visible + * - empty_bibliography (error) — block has no citations + * - heading_missing (warning) — headingText is blank; no heading visible + * - raw_url_link_text (warning) — citation uses a raw URL/DOI as link text + * - all_metadata_disabled (warning) — all metadata outputs are turned off * * @see https://github.com/troychaplin/block-accessibility-checks * @since 1.1.0 @@ -18,6 +20,26 @@ import { addFilter } from '@wordpress/hooks'; const BLOCK_TYPE = 'bibliography-builder/bibliography'; const NAMESPACE = 'borges-bibliography-builder/bac-validation'; +const RAW_URL_RE = /^https?:\/\/\S+$|^10\.\d{4,}\/\S+$/i; + +function isRawUrlText(text) { + return typeof text === 'string' && RAW_URL_RE.test(text.trim()); +} + +function hasRawUrlLinks(citations) { + if (!Array.isArray(citations)) { + return false; + } + return citations.some((csl) => { + const url = csl?.URL || csl?.DOI; + const title = csl?.title; + if (!url) { + return false; + } + return !title || isRawUrlText(title); + }); +} + /** * Pure validation logic — exported for unit testing. * @@ -40,6 +62,15 @@ export function validateBibliographyBlock(attributes, checkName) { ); } + if (checkName === 'raw_url_link_text') { + return !hasRawUrlLinks(attributes.citations); + } + + if (checkName === 'all_metadata_disabled') { + const { outputJsonLd, outputCoins, outputCslJson } = attributes; + return !!(outputJsonLd || outputCoins || outputCslJson); + } + // Unknown checks pass through unchanged. return true; } @@ -65,4 +96,4 @@ export function bacValidateBlock(isValid, blockType, attributes, checkName) { return validateBibliographyBlock(attributes, checkName); } -addFilter('ba11yc_validate_block', NAMESPACE, bacValidateBlock); +addFilter('ba11yc.validateBlock', NAMESPACE, bacValidateBlock); diff --git a/src/validation.test.js b/src/validation.test.js index ddc4a7c..af30677 100644 --- a/src/validation.test.js +++ b/src/validation.test.js @@ -9,9 +9,9 @@ jest.mock('@wordpress/hooks', () => ({ })); describe('BAC filter registration', () => { - it('registers the documented BAC validation filter', () => { + it('registers the ba11yc.validateBlock filter (dot-separated, v4 API)', () => { expect(addFilter).toHaveBeenCalledWith( - 'ba11yc_validate_block', + 'ba11yc.validateBlock', 'borges-bibliography-builder/bac-validation', bacValidateBlock ); @@ -98,6 +98,118 @@ describe('validateBibliographyBlock', () => { }); }); + describe('raw_url_link_text check', () => { + it('passes when citations have descriptive titles', () => { + expect( + validateBibliographyBlock( + { + citations: [ + { URL: 'https://example.com', title: 'My Paper' }, + ], + }, + 'raw_url_link_text' + ) + ).toBe(true); + }); + + it('fails when a citation has a URL but no title', () => { + expect( + validateBibliographyBlock( + { citations: [{ URL: 'https://example.com/paper' }] }, + 'raw_url_link_text' + ) + ).toBe(false); + }); + + it('fails when a citation has a DOI but no title', () => { + expect( + validateBibliographyBlock( + { citations: [{ DOI: '10.1234/example' }] }, + 'raw_url_link_text' + ) + ).toBe(false); + }); + + it('fails when a citation title is itself a raw URL', () => { + expect( + validateBibliographyBlock( + { + citations: [ + { + URL: 'https://example.com', + title: 'https://example.com', + }, + ], + }, + 'raw_url_link_text' + ) + ).toBe(false); + }); + + it('passes when citations array is empty', () => { + expect( + validateBibliographyBlock( + { citations: [] }, + 'raw_url_link_text' + ) + ).toBe(true); + }); + }); + + describe('all_metadata_disabled check', () => { + it('fails when all three metadata outputs are disabled', () => { + expect( + validateBibliographyBlock( + { + outputJsonLd: false, + outputCoins: false, + outputCslJson: false, + }, + 'all_metadata_disabled' + ) + ).toBe(false); + }); + + it('passes when outputJsonLd is enabled', () => { + expect( + validateBibliographyBlock( + { + outputJsonLd: true, + outputCoins: false, + outputCslJson: false, + }, + 'all_metadata_disabled' + ) + ).toBe(true); + }); + + it('passes when outputCoins is enabled', () => { + expect( + validateBibliographyBlock( + { + outputJsonLd: false, + outputCoins: true, + outputCslJson: false, + }, + 'all_metadata_disabled' + ) + ).toBe(true); + }); + + it('passes when outputCslJson is enabled', () => { + expect( + validateBibliographyBlock( + { + outputJsonLd: false, + outputCoins: false, + outputCslJson: true, + }, + 'all_metadata_disabled' + ) + ).toBe(true); + }); + }); + describe('unknown check name', () => { it('returns true (passes) for unrecognised check names', () => { expect(validateBibliographyBlock({}, 'some_future_check')).toBe( diff --git a/tests/e2e/a11y.spec.js b/tests/e2e/a11y.spec.js index 7d1e880..4e2545b 100644 --- a/tests/e2e/a11y.spec.js +++ b/tests/e2e/a11y.spec.js @@ -544,10 +544,14 @@ test.describe('Bibliography block accessibility gate', () => { await dismissEditorOverlay(page); // BAC may not be active in all environments; skip gracefully if absent. - // The plugin exposes a global `window.BlockAccessibilityChecks` when loaded. + // Detect via the block-accessibility-checks Redux store (v4 API). const bacPresent = await page .evaluate( - () => typeof window.BlockAccessibilityChecks !== 'undefined' + () => + typeof window.wp !== 'undefined' && + typeof window.wp.data !== 'undefined' && + typeof window.wp.data.select('block-accessibility-checks') !== + 'undefined' ) .catch(() => false); diff --git a/tests/phpunit/A11yIntegrationTest.php b/tests/phpunit/A11yIntegrationTest.php index 41fc97d..ba88a63 100644 --- a/tests/phpunit/A11yIntegrationTest.php +++ b/tests/phpunit/A11yIntegrationTest.php @@ -2,54 +2,114 @@ use PHPUnit\Framework\TestCase; -final class BibliographyBuilderFakeA11yRegistry { - public $checks = array(); - - public function register_check( $block_name, $check_name, $args ) { - $this->checks[] = array( - 'block_name' => $block_name, - 'check_name' => $check_name, - 'args' => $args, - ); - } -} - final class A11yIntegrationTest extends TestCase { + /** Calls recorded by the stub ba11yc_register_block_check(). */ + private array $registered_checks = array(); + protected function setUp(): void { parent::setUp(); bibliography_builder_test_reset_state(); + $this->registered_checks = array(); } - public function test_register_a11y_checks_ignores_invalid_registry(): void { - $this->assertNull( bibliography_builder_register_a11y_checks( null ) ); + // ------------------------------------------------------------------------- + // PHP registration + // ------------------------------------------------------------------------- - $result = bibliography_builder_register_a11y_checks( new stdClass() ); + public function test_register_a11y_checks_is_noop_when_function_absent(): void { + // When ba11yc_register_block_check() is not defined the function must + // return without throwing. + if ( function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is defined in this environment.' ); + } - $this->assertNull( $result ); + $this->assertNull( bibliography_builder_register_a11y_checks() ); } - public function test_register_a11y_checks_registers_bibliography_checks(): void { - $registry = new BibliographyBuilderFakeA11yRegistry(); - - bibliography_builder_register_a11y_checks( $registry ); - - $this->assertCount( 2, $registry->checks ); - $this->assertSame( 'bibliography-builder/bibliography', $registry->checks[0]['block_name'] ); - $this->assertSame( 'empty_bibliography', $registry->checks[0]['check_name'] ); - $this->assertSame( 'error', $registry->checks[0]['args']['type'] ); - $this->assertSame( 'accessibility', $registry->checks[0]['args']['category'] ); - $this->assertSame( 'bibliography-builder/bibliography', $registry->checks[1]['block_name'] ); - $this->assertSame( 'heading_missing', $registry->checks[1]['check_name'] ); - $this->assertSame( 'warning', $registry->checks[1]['args']['type'] ); - $this->assertSame( 'accessibility', $registry->checks[1]['args']['category'] ); - $this->assertNotEmpty( $registry->checks[1]['args']['error_msg'] ); - $this->assertSame( - $registry->checks[1]['args']['error_msg'], - $registry->checks[1]['args']['warning_msg'] - ); + public function test_register_a11y_checks_registers_all_four_checks(): void { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); + } + + bibliography_builder_register_a11y_checks(); + + $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); + + $this->assertCount( 4, $calls ); + + $ids = array_column( array_column( $calls, 'args' ), 'name' ); + $this->assertContains( 'empty_bibliography', $ids ); + $this->assertContains( 'heading_missing', $ids ); + $this->assertContains( 'raw_url_link_text', $ids ); + $this->assertContains( 'all_metadata_disabled', $ids ); } + public function test_register_a11y_checks_uses_correct_block_type(): void { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); + } + + bibliography_builder_register_a11y_checks(); + + $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); + + foreach ( $calls as $call ) { + $this->assertSame( 'bibliography-builder/bibliography', $call['block_type'] ); + } + } + + public function test_register_a11y_checks_uses_v4_args_shape(): void { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); + } + + bibliography_builder_register_a11y_checks(); + + $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); + + foreach ( $calls as $call ) { + $args = $call['args']; + $this->assertArrayHasKey( 'namespace', $args, 'namespace key required in v4' ); + $this->assertSame( 'borges-bibliography-builder', $args['namespace'] ); + $this->assertArrayHasKey( 'level', $args, 'level key required in v4' ); + $this->assertArrayNotHasKey( 'type', $args, 'type key removed in v4' ); + $this->assertNotEmpty( $args['error_msg'] ); + } + } + + public function test_empty_bibliography_check_is_error(): void { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); + } + + bibliography_builder_register_a11y_checks(); + + $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); + $check = $this->findCheck( $calls, 'empty_bibliography' ); + + $this->assertSame( 'error', $check['args']['level'] ); + } + + public function test_heading_missing_check_is_warning(): void { + if ( ! function_exists( 'ba11yc_register_block_check' ) ) { + $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); + } + + bibliography_builder_register_a11y_checks(); + + $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); + $check = $this->findCheck( $calls, 'heading_missing' ); + + $this->assertSame( 'warning', $check['args']['level'] ); + $this->assertNotEmpty( $check['args']['error_msg'] ); + $this->assertSame( $check['args']['error_msg'], $check['args']['warning_msg'] ); + } + + // ------------------------------------------------------------------------- + // Enqueue behaviour (unchanged from v3 — handle already correct) + // ------------------------------------------------------------------------- + public function test_enqueue_a11y_validation_is_noop_when_bac_script_is_absent(): void { bibliography_builder_enqueue_a11y_validation(); @@ -143,4 +203,17 @@ public function test_block_init_registers_block_metadata_directory(): void { $GLOBALS['bibliography_builder_test_registered_block_types'] ); } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function findCheck( array $calls, string $name ): array { + foreach ( $calls as $call ) { + if ( ( $call['args']['name'] ?? '' ) === $name ) { + return $call; + } + } + $this->fail( "Check '{$name}' was not registered." ); + } } From d68b1a3a9c60f3031bc2864f06a8840acb166432 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Wed, 8 Jul 2026 09:30:50 -0300 Subject: [PATCH 2/3] fix lint js errors --- tests/e2e/a11y.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/a11y.spec.js b/tests/e2e/a11y.spec.js index 4e2545b..a572aea 100644 --- a/tests/e2e/a11y.spec.js +++ b/tests/e2e/a11y.spec.js @@ -550,8 +550,9 @@ test.describe('Bibliography block accessibility gate', () => { () => typeof window.wp !== 'undefined' && typeof window.wp.data !== 'undefined' && - typeof window.wp.data.select('block-accessibility-checks') !== - 'undefined' + typeof window.wp.data.select( + 'block-accessibility-checks' + ) !== 'undefined' ) .catch(() => false); From d83764886676d2e5eb2143cfc5c89242be5335f1 Mon Sep 17 00:00:00 2001 From: Dan Knauss <273554+dknauss@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:54:19 -0600 Subject: [PATCH 3/3] Fix BAC v4 integration review issues --- bibliography-builder.php | 41 ++++++++---------- src/lib/block-accessibility-checks.test.js | 7 +-- src/validation.js | 9 ++-- src/validation.test.js | 43 +++++++++++++++---- tests/phpunit/A11yIntegrationTest.php | 50 +++++++--------------- tests/phpunit/bootstrap.php | 9 ++++ 6 files changed, 86 insertions(+), 73 deletions(-) diff --git a/bibliography-builder.php b/bibliography-builder.php index c482c5b..964b880 100644 --- a/bibliography-builder.php +++ b/bibliography-builder.php @@ -1804,10 +1804,11 @@ function bibliography_builder_block_init() { * active. If BAC is not present, this function is never called. * * Registered checks: - * - empty_bibliography (error) — block has no citations. - * - heading_missing (warning) — headingText is blank; no visible heading. + * - empty_bibliography (error) - block has no citations. + * - heading_missing (warning) - citations exist with no visible heading. + * - raw_url_link_text (warning) - citation link text is a raw URL/DOI. + * - all_metadata_disabled (warning) - metadata outputs are disabled. * - * @param object $registry BAC check registry instance. * @since 1.1.0 */ function bibliography_builder_register_a11y_checks() { @@ -1815,7 +1816,15 @@ function bibliography_builder_register_a11y_checks() { return; } - $heading_missing_message = __( + $raw_url_link_text_message = __( + 'One or more citations use a URL or DOI as link text. Add a descriptive title for screen readers.', + 'borges-bibliography-builder' + ); + $all_metadata_disabled_message = __( + 'All machine-readable metadata outputs are disabled. Enable JSON-LD, COinS, or CSL-JSON for interoperability.', + 'borges-bibliography-builder' + ); + $heading_missing_message = __( 'Bibliography block has no heading. Screen reader users may not find this section.', 'borges-bibliography-builder' ); @@ -1856,14 +1865,8 @@ function bibliography_builder_register_a11y_checks() { array( 'namespace' => 'borges-bibliography-builder', 'name' => 'raw_url_link_text', - 'error_msg' => __( - 'One or more citations have a URL or DOI as the only identifier. A descriptive title helps screen reader users understand the link destination.', - 'borges-bibliography-builder' - ), - 'warning_msg' => __( - 'One or more citations have a URL or DOI as the only identifier. A descriptive title helps screen reader users understand the link destination.', - 'borges-bibliography-builder' - ), + 'error_msg' => $raw_url_link_text_message, + 'warning_msg' => $raw_url_link_text_message, 'description' => __( 'Citations with a URL or DOI should have a descriptive title as link text.', 'borges-bibliography-builder' @@ -1879,16 +1882,10 @@ function bibliography_builder_register_a11y_checks() { array( 'namespace' => 'borges-bibliography-builder', 'name' => 'all_metadata_disabled', - 'error_msg' => __( - 'All machine-readable metadata outputs (JSON-LD, COinS, CSL-JSON) are disabled. Enabling at least one improves citation-manager interoperability.', - 'borges-bibliography-builder' - ), - 'warning_msg' => __( - 'All machine-readable metadata outputs (JSON-LD, COinS, CSL-JSON) are disabled. Enabling at least one improves citation-manager interoperability.', - 'borges-bibliography-builder' - ), + 'error_msg' => $all_metadata_disabled_message, + 'warning_msg' => $all_metadata_disabled_message, 'description' => __( - 'Enable at least one metadata output (JSON-LD, COinS, or CSL-JSON) to improve citation-manager interoperability.', + 'Enable at least one metadata output to improve citation-manager interoperability.', 'borges-bibliography-builder' ), 'level' => 'warning', @@ -1897,7 +1894,7 @@ function bibliography_builder_register_a11y_checks() { ) ); } -add_action( 'ba11yc_ready', 'bibliography_builder_register_a11y_checks' ); +add_action( 'ba11yc_ready', 'bibliography_builder_register_a11y_checks', 10, 0 ); /** * Conditionally enqueue the BAC validation script in the block editor. diff --git a/src/lib/block-accessibility-checks.test.js b/src/lib/block-accessibility-checks.test.js index 6bc62e1..a301315 100644 --- a/src/lib/block-accessibility-checks.test.js +++ b/src/lib/block-accessibility-checks.test.js @@ -1,8 +1,5 @@ -// The window.BlockAccessibilityChecks integration path was removed in v4. -// Check logic is now tested via src/validation.test.js. +import * as legacyIntegration from './block-accessibility-checks'; test('BAC v3 window-global path removed', () => { - // All four checks are now registered via ba11yc_register_block_check() in - // PHP and validated via the ba11yc.validateBlock filter in validation.js. - expect(true).toBe(true); + expect(legacyIntegration.registerBACChecks).toBeUndefined(); }); diff --git a/src/validation.js b/src/validation.js index c7c9302..cad6af9 100644 --- a/src/validation.js +++ b/src/validation.js @@ -56,10 +56,13 @@ export function validateBibliographyBlock(attributes, checkName) { } if (checkName === 'heading_missing') { - return ( + const hasCitations = + Array.isArray(attributes.citations) && + attributes.citations.length > 0; + const hasHeading = typeof attributes.headingText === 'string' && - attributes.headingText.trim().length > 0 - ); + attributes.headingText.trim().length > 0; + return !hasCitations || hasHeading; } if (checkName === 'raw_url_link_text') { diff --git a/src/validation.test.js b/src/validation.test.js index af30677..2d5734b 100644 --- a/src/validation.test.js +++ b/src/validation.test.js @@ -67,31 +67,56 @@ describe('validateBibliographyBlock', () => { it('passes when headingText is a non-empty string', () => { expect( validateBibliographyBlock( - { headingText: 'References' }, + { + citations: [{ csl: { title: 'Example' } }], + headingText: 'References', + }, 'heading_missing' ) ).toBe(true); }); - it('fails when headingText is an empty string', () => { + it('passes when headingText is empty and the block has no citations', () => { + expect( + validateBibliographyBlock( + { citations: [], headingText: '' }, + 'heading_missing' + ) + ).toBe(true); + }); + + it('passes when citations attribute is missing', () => { + expect(validateBibliographyBlock({}, 'heading_missing')).toBe(true); + }); + + it('fails when citations exist and headingText is an empty string', () => { expect( validateBibliographyBlock( - { headingText: '' }, + { + citations: [{ csl: { title: 'Example' } }], + headingText: '', + }, 'heading_missing' ) ).toBe(false); }); - it('fails when headingText is missing', () => { - expect(validateBibliographyBlock({}, 'heading_missing')).toBe( - false - ); + it('fails when citations exist and headingText is missing', () => { + expect( + validateBibliographyBlock( + { citations: [{ csl: { title: 'Example' } }] }, + 'heading_missing' + ) + ).toBe(false); }); - it('fails when headingText is whitespace-only', () => { + it('fails when citations exist and headingText is whitespace-only', () => { expect( validateBibliographyBlock( - { headingText: ' ' }, + { + citations: [{ csl: { title: 'Example' } }], + headingText: ' ', + }, 'heading_missing' ) ).toBe(false); diff --git a/tests/phpunit/A11yIntegrationTest.php b/tests/phpunit/A11yIntegrationTest.php index ba88a63..d78f9cf 100644 --- a/tests/phpunit/A11yIntegrationTest.php +++ b/tests/phpunit/A11yIntegrationTest.php @@ -4,34 +4,16 @@ final class A11yIntegrationTest extends TestCase { - /** Calls recorded by the stub ba11yc_register_block_check(). */ - private array $registered_checks = array(); - protected function setUp(): void { parent::setUp(); bibliography_builder_test_reset_state(); - $this->registered_checks = array(); } // ------------------------------------------------------------------------- // PHP registration // ------------------------------------------------------------------------- - public function test_register_a11y_checks_is_noop_when_function_absent(): void { - // When ba11yc_register_block_check() is not defined the function must - // return without throwing. - if ( function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is defined in this environment.' ); - } - - $this->assertNull( bibliography_builder_register_a11y_checks() ); - } - public function test_register_a11y_checks_registers_all_four_checks(): void { - if ( ! function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); - } - bibliography_builder_register_a11y_checks(); $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); @@ -46,10 +28,6 @@ public function test_register_a11y_checks_registers_all_four_checks(): void { } public function test_register_a11y_checks_uses_correct_block_type(): void { - if ( ! function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); - } - bibliography_builder_register_a11y_checks(); $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); @@ -60,10 +38,6 @@ public function test_register_a11y_checks_uses_correct_block_type(): void { } public function test_register_a11y_checks_uses_v4_args_shape(): void { - if ( ! function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); - } - bibliography_builder_register_a11y_checks(); $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); @@ -79,10 +53,6 @@ public function test_register_a11y_checks_uses_v4_args_shape(): void { } public function test_empty_bibliography_check_is_error(): void { - if ( ! function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); - } - bibliography_builder_register_a11y_checks(); $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); @@ -92,10 +62,6 @@ public function test_empty_bibliography_check_is_error(): void { } public function test_heading_missing_check_is_warning(): void { - if ( ! function_exists( 'ba11yc_register_block_check' ) ) { - $this->markTestSkipped( 'ba11yc_register_block_check is not defined in this environment.' ); - } - bibliography_builder_register_a11y_checks(); $calls = $GLOBALS['bibliography_builder_test_bac_register_calls'] ?? array(); @@ -106,6 +72,22 @@ public function test_heading_missing_check_is_warning(): void { $this->assertSame( $check['args']['error_msg'], $check['args']['warning_msg'] ); } + public function test_bac_ready_action_passes_no_arguments(): void { + $matching_actions = array_filter( + $GLOBALS['bibliography_builder_test_added_actions'], + static function ( $action ) { + return 'ba11yc_ready' === $action['hook_name'] + && 'bibliography_builder_register_a11y_checks' === $action['callback']; + } + ); + + $this->assertNotEmpty( $matching_actions ); + + $action = array_values( $matching_actions )[0]; + + $this->assertSame( 0, $action['accepted_args'] ); + } + // ------------------------------------------------------------------------- // Enqueue behaviour (unchanged from v3 — handle already correct) // ------------------------------------------------------------------------- diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 66a54bd..8f4bec3 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -19,6 +19,7 @@ $GLOBALS['bibliography_builder_test_transients'] = array(); $GLOBALS['bibliography_builder_test_object_cache'] = array(); $GLOBALS['bibliography_builder_test_using_ext_object_cache'] = false; +$GLOBALS['bibliography_builder_test_bac_register_calls'] = array(); function bibliography_builder_test_reset_state() { $GLOBALS['bibliography_builder_test_posts'] = array(); @@ -35,6 +36,7 @@ function bibliography_builder_test_reset_state() { $GLOBALS['bibliography_builder_test_transients'] = array(); $GLOBALS['bibliography_builder_test_object_cache'] = array(); $GLOBALS['bibliography_builder_test_using_ext_object_cache'] = false; + $GLOBALS['bibliography_builder_test_bac_register_calls'] = array(); } function bibliography_builder_test_set_post( $post_id, $status, $content, $password_required = false ) { @@ -122,6 +124,13 @@ function add_action( $hook_name = '', $callback = null, $priority = 10, $accepte function add_filter() {} +function ba11yc_register_block_check( $block_type, $args ) { + $GLOBALS['bibliography_builder_test_bac_register_calls'][] = array( + 'block_type' => $block_type, + 'args' => $args, + ); +} + function register_block_type( $block_type ) { $GLOBALS['bibliography_builder_test_registered_block_types'][] = $block_type; }