Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
Expand Down
30 changes: 28 additions & 2 deletions resources/assets/debugbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,12 @@
var body = el('div', 'phalcon-debugbar-body');
var row = el('div', 'phalcon-debugbar-row');
var tabs = el('div', 'phalcon-debugbar-tabs');
tabs.setAttribute('role', 'tablist');
var indicators = el('div', 'phalcon-debugbar-indicators');
var toggle = el('button', 'phalcon-debugbar-toggle', '⚡');
toggle.type = 'button';
toggle.title = 'Toggle Phalcon DebugBar';
toggle.setAttribute('aria-label', 'Toggle Phalcon DebugBar');
body.style.display = 'none';

var active = null;
Expand All @@ -679,11 +681,19 @@
var historyTrigger = null;
var activeBeforeHistory = null;

// A metric indicator also opens a panel, but only a tab has aria-selected.
function setSelected(node, selected) {
node.classList.toggle('is-active', selected);
if (node.getAttribute('role') === 'tab') {
node.setAttribute('aria-selected', selected ? 'true' : 'false');
}
}

function closePanel() {
body.style.display = 'none';
active = null;
Array.prototype.forEach.call(row.querySelectorAll('[data-panel-tab]'), function (child) {
child.classList.remove('is-active');
setSelected(child, false);
});
}

Expand Down Expand Up @@ -738,6 +748,7 @@
} else {
mount.classList.remove('is-collapsed');
}
toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}

toggle.addEventListener('click', function () {
Expand All @@ -746,10 +757,23 @@
writeCollapsed(collapsed);
});

// The listener is on the bar, so it gets only the keys pressed while the focus is in the bar.
mount.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') {
return;
}

if (historyOpen) {
setHistoryOpen(false);
} else if (active !== null) {
closePanel();
}
});

function activate(name, tab, entry, type) {
closePanel();
setHistoryOpen(false);
tab.classList.add('is-active');
setSelected(tab, true);
body.innerHTML = '';
var summary = renderSummary(entry.summary);
if (summary) {
Expand Down Expand Up @@ -813,6 +837,8 @@

var tab = el('button', 'phalcon-debugbar-tab');
tab.type = 'button';
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-selected', 'false');
tab.appendChild(el('span', 'phalcon-debugbar-tab-label', label));
if (hasBadge(entry.badge)) {
tab.appendChild(el('span', 'phalcon-debugbar-badge', scalar(entry.badge)));
Expand Down
11 changes: 7 additions & 4 deletions resources/assets/debugbar.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/Debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function __construct()
}

/**
* Clears are variables added previously
* Clears the variables added previously
*
* @return $this
*/
Expand Down Expand Up @@ -289,7 +289,9 @@ public function renderHtml(Throwable $exception): string
}

/**
* Sets if files the exception's backtrace must be showed
* Sets the keys to remove from the request and the server data. The
* array has a `request` and a `server` list of keys. A key matches at
* every depth.
*
* @param array<array-key, mixed> $blacklist
*
Expand Down
97 changes: 97 additions & 0 deletions tests/JavaScript/debugbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
7 changes: 7 additions & 0 deletions tests/JavaScript/support/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading