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
3 changes: 3 additions & 0 deletions .github/workflows/assets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: Test JavaScript.
run: npm run test:js

- name: Test JavaScript mutations.
run: npm run test:mutation

- name: Build assets.
run: npm run build

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ node_modules
.phpunit.result.cache

# mutation testing
.stryker-tmp
reports/mutation

# vagrant (if present)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- fix: guard the user-switch panel against concurrent submits so a double-click (or set + reset in the same tick) sends a single identity-switch request instead of racing two session regenerations.
- test: strengthen mutation coverage and clear PHPStan's result cache before static mutation analysis.
- feat(router): implement Router panel with Current Route and Rules sections.
- fix(toolbar): follow debug tags through adapter query URLs and enforce complete JavaScript mutation coverage.
2 changes: 1 addition & 1 deletion resources/assets/dist/js/toolbar.min.js

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions resources/src/toolbar/brand.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ export function renderYiiBrand(version, configUrl, logoHtml, escapeHtml) {
}

export function renderPhpBrand(version, phpInfoUrl, iconHtml, escapeHtml) {
if (!version) {
return "";
}

var content =
iconHtml + '<span class="brand-version">' + escapeHtml(version) + "</span>";

Expand Down
7 changes: 1 addition & 6 deletions resources/src/toolbar/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,7 @@ YiiDebugToolbar.prototype.followTag = function (tag) {

var previousUrl = url;
var previousTag = this.currentTag;
var nextUrl = toolbarDataUrlForTag(
url,
previousTag || (this.data && this.data.tag),
tag,
window.location.href,
);
var nextUrl = toolbarDataUrlForTag(url, tag, window.location.href);

if (!nextUrl || sameUrl(url, nextUrl)) {
return;
Expand Down
26 changes: 8 additions & 18 deletions resources/src/toolbar/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ const retryDelays = Object.freeze([75, 150, 300, 600, 900]);
* Returns the delay for a toolbar snapshot that Yii3 has not persisted yet.
*/
export function toolbarRetryDelay(status, attempt) {
if (status !== 404 || attempt < 0 || attempt >= retryDelays.length) {
if (
status !== 404 ||
!Number.isInteger(attempt) ||
attempt < 0 ||
attempt >= retryDelays.length
) {
return null;
}

Expand Down Expand Up @@ -39,7 +44,7 @@ export function resolveToolbarLoadRollback(
};
}

export function toolbarDataUrlForTag(url, currentTag, nextTag, baseUrl) {
export function toolbarDataUrlForTag(url, nextTag, baseUrl) {
if (!url || !nextTag) {
return null;
}
Expand All @@ -52,22 +57,7 @@ export function toolbarDataUrlForTag(url, currentTag, nextTag, baseUrl) {
return null;
}

if (parsed.searchParams.has("tag")) {
parsed.searchParams.set("tag", nextTag);

return parsed.href;
}

var segments = parsed.pathname.split("/");
var currentSegment = currentTag ? encodeURIComponent(currentTag) : "";
var currentIndex = currentSegment ? segments.lastIndexOf(currentSegment) : -1;

if (currentIndex !== -1) {
segments[currentIndex] = encodeURIComponent(nextTag);
parsed.pathname = segments.join("/");
} else {
parsed.searchParams.set("tag", nextTag);
}
parsed.searchParams.set("tag", nextTag);

return parsed.href;
}
10 changes: 5 additions & 5 deletions resources/src/toolbar/panel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function hasToolbarItemLink(items) {
return (items || []).some(function (item) {
return Boolean(item && item.url);
return items.some(function (item) {
return Boolean(item.url);
});
}

Expand All @@ -13,7 +13,7 @@ export function renderAjaxProfileLink(profile, profileUrl, nativeUrl, escape) {
'<a class="ajax-link"' +
renderToolbarLinkAttributes(profileUrl, nativeUrl, escape) +
">" +
escape(profile || "profile") +
escape(profile) +
"</a>"
);
}
Expand All @@ -29,11 +29,11 @@ export function renderToolbarLinkAttributes(url, nativeUrl, escape) {
}

export function toolbarPanelContainerTag(panel) {
return panel && panel.url && !hasToolbarItemLink(panel.items) ? "a" : "div";
return panel.url && !hasToolbarItemLink(panel.items) ? "a" : "div";
}

export function toolbarItemTag(item) {
return item && item.url ? "a" : "span";
return item.url ? "a" : "span";
}

export function shouldOpenToolbarDrawer(event, url) {
Expand Down
96 changes: 45 additions & 51 deletions resources/tests/toolbar-runtime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,26 @@ test("builtinIconUrl provides self-contained shared toolbar icons", () => {

test("toolbarRetryDelay retries missing snapshots with bounded backoff", () => {
assert.equal(toolbarRetryDelay(404, 0), 75);
assert.equal(toolbarRetryDelay(404, 1), 150);
assert.equal(toolbarRetryDelay(404, 2), 300);
assert.equal(toolbarRetryDelay(404, 3), 600);
assert.equal(toolbarRetryDelay(404, 4), 900);
assert.equal(toolbarRetryDelay(404, 5), null);
assert.equal(toolbarRetryDelay(404, -1), null);
assert.equal(toolbarRetryDelay(404, Number.NaN), null);
assert.equal(toolbarRetryDelay(404, 0.5), null);
assert.equal(toolbarRetryDelay(500, 0), null);
});

test("toolbar load generations reject stale responses and retries", () => {
var activeGeneration = resolveToolbarLoadGeneration(0);
var staleGeneration = activeGeneration;

assert.equal(activeGeneration, 1);

activeGeneration = resolveToolbarLoadGeneration(activeGeneration);

assert.equal(activeGeneration, 2);
assert.equal(isToolbarLoadCurrent(activeGeneration, staleGeneration), false);
assert.equal(isToolbarLoadCurrent(activeGeneration, activeGeneration), true);
assert.equal(
Expand Down Expand Up @@ -108,67 +117,59 @@ test("toolbar load rollback prefers the last successful snapshot", () => {
);
});

test("toolbar data URLs follow tags across query and pretty-path routes", () => {
test("toolbar data URLs follow tags through query parameters", () => {
var baseUrl = "https://example.test/app";

assert.equal(
toolbarDataUrlForTag(
"/debug/toolbar?tag=request-1&panel=summary",
"request-1",
"request-2",
baseUrl,
),
"https://example.test/debug/toolbar?tag=request-2&panel=summary",
);
assert.equal(
toolbarDataUrlForTag(
"/debug/toolbar/request-1?panel=summary",
"request-1",
"request-2",
baseUrl,
),
"https://example.test/debug/toolbar/request-2?panel=summary",
);
assert.equal(
toolbarDataUrlForTag(
"/debug/toolbar?panel=summary",
null,
"request-2",
baseUrl,
),
toolbarDataUrlForTag("/debug/toolbar?panel=summary", "request-2", baseUrl),
"https://example.test/debug/toolbar?panel=summary&tag=request-2",
);
});

test("toolbar data URL resolution rejects unusable inputs", () => {
assert.equal(toolbarDataUrlForTag("", "request-1", "request-2", ""), null);
assert.equal(
toolbarDataUrlForTag("/debug/toolbar", "request-1", "", ""),
null,
);
var baseUrl = "https://example.test/";

assert.equal(toolbarDataUrlForTag("", "request-2", baseUrl), null);
assert.equal(toolbarDataUrlForTag("/debug/toolbar", "", baseUrl), null);
assert.equal(
toolbarDataUrlForTag(
"https://[invalid",
"request-1",
"request-2",
"https://example.test/",
),
toolbarDataUrlForTag("https://[invalid", "request-2", baseUrl),
null,
);
});

test("toolbar item links remain focusable without nested interactive elements", () => {
var itemOnlyPanel = { items: [{ url: "/debug/request" }], url: null };
var linkedItem = { url: "/debug/request/status" };
var unlinkedItem = { value: "200" };
var itemOnlyPanel = { items: [linkedItem] };
var panelAndItemLinks = {
items: [{ url: "/debug/request/status" }],
items: [linkedItem],
url: "/debug/request",
};

assert.equal(toolbarPanelContainerTag(itemOnlyPanel), "div");
assert.equal(toolbarItemTag(itemOnlyPanel.items[0]), "a");
assert.equal(toolbarItemTag(linkedItem), "a");
assert.equal(toolbarItemTag(unlinkedItem), "span");
assert.equal(toolbarPanelContainerTag(panelAndItemLinks), "div");
assert.equal(toolbarPanelContainerTag({ items: [], url: "/debug" }), "a");
assert.equal(toolbarItemTag({ url: null }), "span");
assert.equal(
toolbarPanelContainerTag({
items: [unlinkedItem, linkedItem],
url: "/debug/request",
}),
"div",
);
assert.equal(
toolbarPanelContainerTag({ items: [unlinkedItem], url: "/debug" }),
"a",
);
assert.equal(toolbarPanelContainerTag({ items: [unlinkedItem] }), "div");
});

test("toolbar native links carry the active theme without changing drawer URLs", () => {
Expand Down Expand Up @@ -253,27 +254,20 @@ test("renderYiiBrand renders unavailable configuration as static content", () =>
});

test("renderPhpBrand renders unavailable PHP info as static content", () => {
var html = renderPhpBrand(
"8.5.9",
null,
'<span class="icon"></span>',
String,
assert.equal(
renderPhpBrand("8.5.9", null, '<span class="icon"></span>', String),
'<span class="brand-link brand-link-php brand-static" title="PHP 8.5.9 — phpinfo unavailable"><span class="icon"></span><span class="brand-version">8.5.9</span></span>',
);

assert.match(html, /^<span class="brand-link brand-link-php brand-static"/);
assert.match(html, /PHP 8\.5\.9 — phpinfo unavailable/);
assert.doesNotMatch(html, /href=|target=/);
});

test("renderPhpBrand links available PHP info", () => {
var html = renderPhpBrand(
"8.5.9",
"/debug/php-info",
'<span class="icon"></span>',
String,
assert.equal(
renderPhpBrand(
"8.5.9",
"/debug/php-info",
'<span class="icon"></span>',
String,
),
'<a class="brand-link brand-link-php" href="/debug/php-info" target="_blank" rel="noopener" title="PHP 8.5.9 — open phpinfo in a new tab"><span class="icon"></span><span class="brand-version">8.5.9</span></a>',
);

assert.match(html, /^<a class="brand-link brand-link-php"/);
assert.match(html, /href="\/debug\/php-info"/);
assert.match(html, /target="_blank"/);
});
6 changes: 3 additions & 3 deletions stryker.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ const config = {
concurrency: 4,
reporters: ["clear-text", "progress", "html"],
thresholds: {
high: 90,
low: 75,
break: 70,
high: 100,
low: 100,
break: 100,
},
};

Expand Down
Loading
Loading