diff --git a/CHANGELOG.md b/CHANGELOG.md index fca3204..ebb1586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas - Debug page: copy on `http://` pages, show "Copy failed" when a copy fails, and encode the paths in the PhpStorm and VS Code links. [#101](https://github.com/phalcon/debugbar/issues/101) - Keep host CSS out of the bar tables, code blocks and buttons, stop history requests after 10 seconds, and show "Unable to load" in a history row that fails. [#102](https://github.com/phalcon/debugbar/issues/102) - Pin `pie.phar` to a release with a sha256 check, build the PHP 8.1 Docker image, declare `ext-mbstring`, and list the Phalcon version that each feature needs. [#104](https://github.com/phalcon/debugbar/issues/104) +- Name the collapse toggle and the tabs for screen readers, close the open panel or the history with Escape, and fix two `Phalcon\Debug` docblocks. [#106](https://github.com/phalcon/debugbar/issues/106) - Test HTML writes, the collapse state and `localStorage` errors in the JS tests, and keep every listener in the fake DOM. [#103](https://github.com/phalcon/debugbar/issues/103) ## [0.4.0](https://github.com/phalcon/debugbar/releases/tag/v0.4.0) (2026-07-14) diff --git a/docs/index.md b/docs/index.md index 45cff61..1e1cf55 100644 --- a/docs/index.md +++ b/docs/index.md @@ -642,7 +642,7 @@ The `request`, `config`, `session`, and `logger` collectors run their data throu The `database` panel and request history show and store query bindings as they are, because generated names such as `:APL0` do not match a key. The redactor does not change log messages, exception messages, or stack traces. -`Phalcon\DebugBar\Security\AccessGate` decides who receives the bar within an allowed environment. An empty IP allowlist admits any client. When both an allowlist and a callback are given, both must pass. Only `true` from the callback allows the client. When the callback or a collector throws, that response gets no bar, and the page of the application does not change. The callback can run more than once for a request, for example for a server error in an ADR application with `debug_page` on. Do not put side effects in the callback. +`Phalcon\DebugBar\Security\AccessGate` decides who receives the bar within an allowed environment. An empty IP allowlist admits any client. The allowlist compares the address exactly. On a host that has IPv4 and IPv6, a local client can come as `::1`, so add `::1` next to `127.0.0.1`. Behind a proxy on the same host, `REMOTE_ADDR` is the address of the proxy, for example `127.0.0.1`, for every client, so the allowlist then lets every client through. When both an allowlist and a callback are given, both must pass. Only `true` from the callback allows the client. When the callback or a collector throws, that response gets no bar, and the page of the application does not change. The callback can run more than once for a request, for example for a server error in an ADR application with `debug_page` on. Do not put side effects in the callback. ```php $blacklist * diff --git a/tests/JavaScript/debugbar.test.js b/tests/JavaScript/debugbar.test.js index 230df23..b6055bf 100644 --- a/tests/JavaScript/debugbar.test.js +++ b/tests/JavaScript/debugbar.test.js @@ -1290,3 +1290,100 @@ test('the collapse toggle works when localStorage throws', function () { dom.findByClass(mount, 'phalcon-debugbar-toggle').click(); assert.equal(mount.classList.contains('is-collapsed'), true); }); + +function twoTabsPayload() { + return { + data: {messages: {panel: []}, route: {panel: []}}, + meta: { + widgets: { + messages: {label: 'Messages', panel: 'list'}, + route: {label: 'Route', panel: 'list'} + } + } + }; +} + +function selectedTabs(tabs) { + return tabs.map(function (tab) { + return tab.getAttribute('aria-selected'); + }); +} + +test('the collapse toggle has a name and reports whether the bar is open', function () { + var mount = renderPayload(messagesPayload()); + var toggle = dom.findByClass(mount, 'phalcon-debugbar-toggle'); + + assert.equal(toggle.getAttribute('aria-label'), 'Toggle Phalcon DebugBar'); + assert.equal(toggle.getAttribute('aria-expanded'), 'true'); + + toggle.click(); + + assert.equal(toggle.getAttribute('aria-expanded'), 'false'); +}); + +test('the tabs are an ARIA tab list that marks the selected tab', function () { + var mount = renderPayload(twoTabsPayload()); + var tabs = mount.querySelectorAll('.phalcon-debugbar-tab'); + + assert.equal(dom.findByClass(mount, 'phalcon-debugbar-tabs').getAttribute('role'), 'tablist'); + assert.deepEqual(tabs.map(function (tab) { + return tab.getAttribute('role'); + }), ['tab', 'tab']); + assert.deepEqual(selectedTabs(tabs), ['false', 'false']); + + tabs[1].click(); + assert.deepEqual(selectedTabs(tabs), ['false', 'true']); + + tabs[0].click(); + assert.deepEqual(selectedTabs(tabs), ['true', 'false']); + + tabs[0].click(); + assert.deepEqual(selectedTabs(tabs), ['false', 'false']); +}); + +test('Escape in the bar closes the open panel, and other keys do nothing', function () { + var mount = renderPayload(twoTabsPayload()); + var tab = mount.querySelectorAll('.phalcon-debugbar-tab')[0]; + var body = dom.findByClass(mount, 'phalcon-debugbar-body'); + + tab.click(); + mount.keydown('Enter'); + assert.equal(body.style.display, 'block'); + + mount.keydown('Escape'); + assert.equal(body.style.display, 'none'); + assert.equal(tab.classList.contains('is-active'), false); + assert.equal(tab.getAttribute('aria-selected'), 'false'); +}); + +test('Escape in the bar closes the request history', async function () { + var mount = renderPayload({ + data: { + request: {panel: {Method: 'GET', URI: '/orders'}, metrics: {method: 'GET', uri: '/orders'}}, + history: {panel: {url: '/_debugbar/open'}} + }, + meta: { + widgets: { + request: { + label: 'Request', + panel: 'grid', + request: {method: ['metrics', 'method'], uri: ['metrics', 'uri']} + }, + history: {label: 'History', panel: 'history'} + } + } + }, { + fetch: function () { + return Promise.resolve(response({requests: []})); + } + }); + + dom.findByClass(mount, 'phalcon-debugbar-request-control').click(); + await dom.flushPromises(); + assert.equal(dom.findByClass(mount, 'phalcon-debugbar-history-browser').style.display, 'block'); + + mount.keydown('Escape'); + + assert.equal(dom.findByClass(mount, 'phalcon-debugbar-history-browser').style.display, 'none'); + assert.equal(dom.findByClass(mount, 'phalcon-debugbar-request-control').getAttribute('aria-expanded'), 'false'); +}); diff --git a/tests/JavaScript/support/dom.js b/tests/JavaScript/support/dom.js index 361c4f2..2330651 100644 --- a/tests/JavaScript/support/dom.js +++ b/tests/JavaScript/support/dom.js @@ -116,6 +116,13 @@ TestElement.prototype.click = function () { } }; +// Sends a key to the element, as when the focus is inside it. +TestElement.prototype.keydown = function (key) { + (this._listeners.keydown || []).forEach(function (listener) { + listener({key: key}); + }); +}; + TestElement.prototype.querySelectorAll = function (selector) { var className = selector.charAt(0) === '.' ? selector.slice(1) : ''; var attribute = /^\[([^\]]+)\]$/.exec(selector);