ILMismatch", html);
Assert.Contains("data-il-file=\"match.dll_IL.txt\"", html);
Assert.Contains("data-il-file=\"mismatch.dll_IL.txt\"", html);
- Assert.Contains("class=\"copy-icon il-path-pair-icon\"", html);
+ Assert.Equal(1, html.Split("data-il-file=\"match.dll_IL.txt\"", StringSplitOptions.None).Length - 1);
+ Assert.Equal(2, html.Split("data-il-file=\"mismatch.dll_IL.txt\"", StringSplitOptions.None).Length - 1);
+ Assert.Contains("class=\"copy-icon path-pair-icon\"", html);
Assert.Contains("onclick=\"copyIlPaths(this)\"", html);
Assert.Contains("Copy the quoted old/new absolute IL text paths for use with a text-based diff tool.", html);
Assert.Contains("background: color-mix(in srgb, var(--color-surface) 60%, transparent);", html);
@@ -175,6 +180,42 @@ public void GenerateDiffReportHtml_ShouldOutputIlTextFalse_OmitsIlPathMetadataAn
Assert.DoesNotContain("data-il-file=\"", html);
}
+ [Fact]
+ public void GenerateDiffReportHtml_TextRows_HaveDistinctOldNewComparedFilePathCopyButtons()
+ {
+ var (oldDir, newDir, reportDir) = MakeDirs("text-path-copy-btn");
+
+ const string matchPath = "config/match.json";
+ const string mismatchPath = "config/mismatch.json";
+ _resultLists.AddUnchangedFileRelativePath(matchPath);
+ _resultLists.RecordDiffDetail(matchPath, FileDiffResultLists.DiffDetailResult.TextMatch);
+ _resultLists.AddModifiedFileRelativePath(mismatchPath);
+ _resultLists.RecordDiffDetail(mismatchPath, FileDiffResultLists.DiffDetailResult.TextMismatch);
+ _resultLists.RecordNewFileTimestampOlderThanOldWarning(
+ mismatchPath, "2026-03-15 10:00:00", "2026-03-15 09:00:00");
+
+ var builder = CreateConfigBuilder();
+ builder.ShouldWarnWhenNewFileTimestampIsOlderThanOldFileTimestamp = true;
+ var config = builder.Build();
+ _service.GenerateDiffReportHtml(CreateReportContext(oldDir, newDir, reportDir, config));
+
+ var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME));
+
+ Assert.Contains("TextMatch", html);
+ Assert.Contains("TextMismatch", html);
+ Assert.Contains($"data-text-old-prefix=\"{Path.GetFullPath(oldDir)}{Path.DirectorySeparatorChar}\"", html);
+ Assert.Contains($"data-text-new-prefix=\"{Path.GetFullPath(newDir)}{Path.DirectorySeparatorChar}\"", html);
+ string platformMatchPath = matchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ string platformMismatchPath = mismatchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ string matchFileAttribute = $"data-text-file=\"{platformMatchPath}\"";
+ string mismatchFileAttribute = $"data-text-file=\"{platformMismatchPath}\"";
+ Assert.Equal(1, html.Split(matchFileAttribute, StringSplitOptions.None).Length - 1);
+ Assert.Equal(2, html.Split(mismatchFileAttribute, StringSplitOptions.None).Length - 1);
+ Assert.Contains("class=\"btn-copy-path btn-copy-text-path\"", html);
+ Assert.Contains("onclick=\"copyTextPaths(this)\"", html);
+ Assert.Contains("Copy the quoted old/new absolute file paths for use with a text-based diff tool.", html);
+ }
+
// ── Req8: Row hover highlight / 行ホバーハイライト ──────────────────────
[Fact]
diff --git a/JsTests/diff_report.test.js b/JsTests/diff_report.test.js
index b00c265f..4912363c 100644
--- a/JsTests/diff_report.test.js
+++ b/JsTests/diff_report.test.js
@@ -1380,6 +1380,134 @@ describe('copyIlPaths', () => {
});
});
+// ─── copyTextPaths ──────────────────────────────────────────────────────────
+// テキスト比較対象ファイルの新旧絶対パスコピーのテスト
+describe('copyTextPaths', () => {
+ function loadTextPathButton() {
+ loadScript({
+ bodyHtml: `
+
+
+ `,
+ });
+ navigator.clipboard = {
+ writeText: (text) => {
+ clipboardText = text;
+ return Promise.resolve();
+ },
+ };
+ window.alert = jest.fn();
+ return document.getElementById('copy-text-btn');
+ }
+
+ let clipboardText;
+
+ beforeEach(() => {
+ clipboardText = '';
+ });
+
+ test('copies quoted old and new Windows drive paths', async () => {
+ const btn = loadTextPathButton();
+ btn.setAttribute('data-text-file', 'config\\app.config');
+ document.body.setAttribute('data-text-old-prefix', 'C:\\Old Folder\\');
+ document.body.setAttribute('data-text-new-prefix', 'D:\\New Folder\\');
+
+ const copied = await window.copyTextPaths(btn);
+
+ expect(copied).toBe(true);
+ expect(clipboardText).toBe('"C:\\Old Folder\\config\\app.config" "D:\\New Folder\\config\\app.config"');
+ expect(btn.classList.contains('is-copy-success')).toBe(true);
+ expect(window.alert).not.toHaveBeenCalled();
+ });
+
+ test('copies quoted old and new Windows UNC paths', async () => {
+ const btn = loadTextPathButton();
+ btn.setAttribute('data-text-file', 'config\\app.config');
+ document.body.setAttribute('data-text-old-prefix', '\\\\old-server\\share\\');
+ document.body.setAttribute('data-text-new-prefix', '\\\\new-server\\share\\');
+
+ await window.copyTextPaths(btn);
+
+ expect(clipboardText).toBe('"\\\\old-server\\share\\config\\app.config" "\\\\new-server\\share\\config\\app.config"');
+ expect(window.alert).not.toHaveBeenCalled();
+ });
+
+ test('copies shell-safe old and new macOS paths', async () => {
+ const btn = loadTextPathButton();
+ btn.setAttribute('data-text-file', 'config/app.config');
+ document.body.setAttribute('data-text-old-prefix', '/Users/test/Old Folder/');
+ document.body.setAttribute('data-text-new-prefix', '/Users/test/New Folder/');
+
+ await window.copyTextPaths(btn);
+
+ expect(clipboardText).toBe("'/Users/test/Old Folder/config/app.config' '/Users/test/New Folder/config/app.config'");
+ expect(window.alert).not.toHaveBeenCalled();
+ });
+
+ test('copies shell-safe old and new Linux paths including a single quote', async () => {
+ const btn = loadTextPathButton();
+ btn.setAttribute('data-text-file', "config/app's.conf");
+ document.body.setAttribute('data-text-old-prefix', '/srv/old release/');
+ document.body.setAttribute('data-text-new-prefix', '/srv/new release/');
+
+ await window.copyTextPaths(btn);
+
+ expect(clipboardText).toBe("'/srv/old release/config/app'\\''s.conf' '/srv/new release/config/app'\\''s.conf'");
+ expect(window.alert).not.toHaveBeenCalled();
+ });
+
+ test('shows button feedback and an alert when compared file path metadata is unavailable', async () => {
+ const btn = loadTextPathButton();
+ btn.setAttribute('data-text-file', 'config/app.config');
+ document.body.setAttribute('data-text-old-prefix', '/old/');
+
+ const copied = await window.copyTextPaths(btn);
+
+ expect(copied).toBe(false);
+ expect(btn.classList.contains('is-copy-error')).toBe(true);
+ expect(window.alert).toHaveBeenCalledWith('Compared text file paths are unavailable.');
+ });
+});
+
+// ─── Text path copy sample consistency ──────────────────────────────────────
+// テキストパスコピーのサンプル整合性
+describe('text path copy sample consistency', () => {
+ test('every TextMatch and TextMismatch sample row has matching old/new absolute paths', () => {
+ const sampleHtml = fs.readFileSync(
+ path.join(__dirname, '..', 'doc', 'samples', 'diff_report.html'),
+ 'utf-8'
+ );
+ const sampleDocument = new DOMParser().parseFromString(sampleHtml, 'text/html');
+ const textRows = sampleDocument.querySelectorAll(
+ 'tr[data-section][data-diff="TextMatch"], tr[data-section][data-diff="TextMismatch"]'
+ );
+
+ expect(textRows).toHaveLength(10);
+ expect(sampleDocument.body.getAttribute('data-text-old-prefix')).toBe('/Users/UserA/workspace/old/');
+ expect(sampleDocument.body.getAttribute('data-text-new-prefix')).toBe('/Users/UserA/workspace/new/');
+ textRows.forEach((row) => {
+ const relativePath = row.querySelector('.path-text').textContent;
+ const button = row.querySelector('.btn-copy-text-path');
+ expect(button).not.toBeNull();
+ expect(button.getAttribute('data-text-file')).toBe(relativePath);
+ expect(button.getAttribute('onclick')).toBe('copyTextPaths(this)');
+
+ const tooltipId = button.getAttribute('aria-describedby');
+ const tooltip = sampleDocument.getElementById(tooltipId);
+ expect(tooltip).not.toBeNull();
+ expect(tooltip.textContent).toBe(
+ 'Copy the quoted old/new absolute file paths for use with a text-based diff tool.'
+ );
+ });
+
+ expect(sampleHtml).toContain('.text-copy-tooltip-wrap');
+ expect(sampleHtml).toContain('.btn-copy-text-path');
+ expect(sampleHtml).toContain('function copyTextPaths(btn)');
+ });
+});
+
// ─── setupLazySection ───────────────────────────────────────────────────────
// 遅延セクションレンダリングのテスト
describe('setupLazySection', () => {
diff --git a/Services/HtmlReport/HtmlReportGenerateService.Helpers.cs b/Services/HtmlReport/HtmlReportGenerateService.Helpers.cs
index 971742e2..125aae1f 100644
--- a/Services/HtmlReport/HtmlReportGenerateService.Helpers.cs
+++ b/Services/HtmlReport/HtmlReportGenerateService.Helpers.cs
@@ -14,8 +14,9 @@ namespace FolderDiffIL4DotNet.Services
public sealed partial class HtmlReportGenerateService
{
private const string COPY_BUTTON_CONTENT = "";
- private const string IL_PATH_COPY_BUTTON_CONTENT = "";
+ private const string PATH_PAIR_COPY_BUTTON_CONTENT = "";
private const string IL_PATH_COPY_TOOLTIP = "Copy the quoted old/new absolute IL text paths for use with a text-based diff tool.";
+ private const string TEXT_PATH_COPY_TOOLTIP = "Copy the quoted old/new absolute file paths for use with a text-based diff tool.";
// ── Table helpers ────────────────────────────────────────────────────
@@ -103,7 +104,14 @@ private static void AppendFileRow(
string ilFileName = TextSanitizer.Sanitize(path) + "_" + Constants.LABEL_IL + ".txt";
string ariaLabel = $"Copy old and new IL text paths for {path}";
string tooltipId = $"il_copy_tip_{sectionPrefix}_{idx}";
- col6Cell += $"{HtmlEncode(IL_PATH_COPY_TOOLTIP)}";
+ col6Cell += $"{HtmlEncode(IL_PATH_COPY_TOOLTIP)}";
+ }
+ if ((diffCat == "TextMatch" || diffCat == "TextMismatch") &&
+ TryBuildComparedFileRelativePath(path, out string comparedFileRelativePath))
+ {
+ string ariaLabel = $"Copy old and new compared file paths for {path}";
+ string tooltipId = $"text_copy_tip_{sectionPrefix}_{idx}";
+ col6Cell += $"{HtmlEncode(TEXT_PATH_COPY_TOOLTIP)}";
}
if (!string.IsNullOrEmpty(importance))
col6Cell += $" {HtmlEncode(importance)}";
@@ -119,6 +127,41 @@ private static void AppendFileRow(
writer.WriteLine("");
}
+ // Normalize compared-file paths with the report generator's native directory separator.
+ // 比較対象ファイルのパスを、レポート生成環境のネイティブなディレクトリ区切りで正規化する。
+ private static bool TryBuildComparedFileRelativePath(
+ string fileRelativePath,
+ out string normalizedRelativePath)
+ {
+ try
+ {
+ normalizedRelativePath = Path.DirectorySeparatorChar == Path.AltDirectorySeparatorChar
+ ? fileRelativePath
+ : fileRelativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+ if (Path.IsPathRooted(normalizedRelativePath))
+ {
+ normalizedRelativePath = "";
+ return false;
+ }
+ _ = Path.GetFullPath(normalizedRelativePath);
+ return true;
+ }
+ catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
+ {
+ normalizedRelativePath = "";
+ return false;
+ }
+ }
+
+ private static string BuildAbsoluteDirectoryPrefix(string folderAbsolutePath)
+ {
+ string fullPath = Path.GetFullPath(folderAbsolutePath);
+ string trimmedPath = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ return string.IsNullOrEmpty(trimmedPath)
+ ? Path.DirectorySeparatorChar.ToString()
+ : trimmedPath + Path.DirectorySeparatorChar;
+ }
+
private static string BuildDiffViewHtml(IReadOnlyListILMatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool.ILMatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool.dotnet-ildasm (version: 0.12.2).NET 8.0TextMatchTextMatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMatchTextMatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatch HighTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool. HighDepUpdateTextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. HighILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. High-Method, Signaturedotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. HighILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. High+Type, +Methoddotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. HighILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. HighPossible Extract, +Methoddotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumPossible Move, Possible Renamedotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumAccessdotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumBodyEditdotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. LowILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. LowPossible Inlinedotnet-ildasm (version: 0.12.2).NET 6.0 → .NET 8.0TextMismatchTextMismatchCopy the quoted old/new absolute file paths for use with a text-based diff tool.ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. HighILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. High-Method, Signaturedotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumPossible Move, Possible Renamedotnet-ildasm (version: 0.12.2).NET 8.0ILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumILMismatchCopy the quoted old/new absolute IL text paths for use with a text-based diff tool. MediumBodyEditdotnet-ildasm (version: 0.12.2).NET 8.0