feat: add copyable cli test command

This commit is contained in:
Test User
2026-06-06 02:14:36 +08:00
parent 1a00aa49a8
commit a216ab5fa5
4 changed files with 142 additions and 0 deletions
+20
View File
@@ -46,6 +46,26 @@ func TestStaticAssetsUseFixedDurationChoices(t *testing.T) {
}
}
func TestStaticAssetsIncludeCopyableClientCommand(t *testing.T) {
html, err := readAsset("index.html")
if err != nil {
t.Fatalf("read index.html: %v", err)
}
if !contains(html, `id="cliCommand"`) || !contains(html, `id="copyCommandButton"`) {
t.Fatalf("index.html missing copyable CLI command controls")
}
app, err := readAsset("app.js")
if err != nil {
t.Fatalf("read app.js: %v", err)
}
if !contains(app, "updateClientCommand") ||
!contains(app, "navigator.clipboard.writeText") ||
!contains(app, "./netstable client -server") {
t.Fatalf("app.js missing generated copyable CLI command behavior")
}
}
func contains(text, pattern string) bool {
return strings.Contains(text, pattern)
}
+48
View File
@@ -2,11 +2,13 @@ const state = {
source: null,
samples: [],
records: [],
clientCommand: "",
};
const els = {
form: document.getElementById("testForm"),
startButton: document.getElementById("startButton"),
copyCommandButton: document.getElementById("copyCommandButton"),
refreshButton: document.getElementById("refreshButton"),
notice: document.getElementById("notice"),
serverState: document.getElementById("serverState"),
@@ -17,6 +19,7 @@ const els = {
connectionState: document.getElementById("connectionState"),
bandwidthLimit: document.getElementById("bandwidthLimit"),
sampleCount: document.getElementById("sampleCount"),
cliCommand: document.getElementById("cliCommand"),
liveChart: document.getElementById("liveChart"),
historyChart: document.getElementById("historyChart"),
eventLog: document.getElementById("eventLog"),
@@ -27,6 +30,10 @@ const DOWNLOAD_BYTES = 4 * 1024 * 1024;
const UPLOAD_BYTES = 2 * 1024 * 1024;
els.form.addEventListener("submit", startTest);
els.copyCommandButton.addEventListener("click", copyClientCommand);
document.querySelectorAll('input[name="durationSeconds"]').forEach(input => {
input.addEventListener("change", updateClientCommand);
});
els.refreshButton.addEventListener("click", loadRecords);
window.addEventListener("resize", () => {
drawLiveChart();
@@ -34,6 +41,7 @@ window.addEventListener("resize", () => {
});
initializeDefaultTarget();
updateClientCommand();
connectHeartbeat();
loadConfig();
loadRecords();
@@ -50,6 +58,30 @@ function initializeDefaultTarget() {
}
}
function updateClientCommand() {
const serverURL = window.location.origin || `http://${window.location.host}`;
const duration = numberValue("durationSeconds") || 30;
const serverArg = shellQuote(serverURL);
state.clientCommand = `./netstable client -server ${serverArg} -duration ${duration}`;
els.cliCommand.textContent = `./netstable client \\\n -server ${serverArg} \\\n -duration ${duration}`;
}
async function copyClientCommand() {
updateClientCommand();
const command = state.clientCommand;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(command);
} else {
fallbackCopy(command);
}
setNotice("命令已复制。");
} catch (error) {
fallbackCopy(command);
setNotice("命令已复制。");
}
}
async function startTest(event) {
event.preventDefault();
closeSource();
@@ -518,6 +550,22 @@ function numberValue(id) {
return selected ? Number(selected.value) : 0;
}
function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
function fallbackCopy(value) {
const textarea = document.createElement("textarea");
textarea.value = value;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "-1000px";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
function average(values) {
if (!values.length) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
+7
View File
@@ -48,6 +48,13 @@
</div>
<button id="startButton" type="submit">开始实时测试</button>
</form>
<div class="command-panel" aria-label="CLI 一键测试命令">
<div class="command-title">
<span>CLI 一键测试命令</span>
<button id="copyCommandButton" class="secondary-button" type="button">复制</button>
</div>
<code id="cliCommand" class="command-text"></code>
</div>
<p id="notice" class="notice"></p>
</section>
+67
View File
@@ -101,6 +101,10 @@ label span {
padding: 18px;
}
.control-panel {
min-width: 0;
}
.form {
display: grid;
gap: 14px;
@@ -218,6 +222,59 @@ button:disabled {
border-color: #bfdbfe;
}
.secondary-button {
min-height: 34px;
padding: 7px 12px;
color: var(--blue);
background: #eff6ff;
border-color: #bfdbfe;
}
.command-panel {
display: grid;
gap: 8px;
min-width: 0;
overflow: hidden;
margin-top: 14px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfdff;
}
.command-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.command-title span {
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.command-text {
display: block;
width: 100%;
min-width: 0;
max-width: 100%;
min-height: 38px;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--ink);
background: #ffffff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-all;
}
.notice {
min-height: 22px;
margin-top: 12px;
@@ -400,6 +457,16 @@ tr:last-child td {
min-height: 36px;
}
.command-title {
display: grid;
grid-template-columns: 1fr;
align-items: stretch;
}
.command-title .secondary-button {
width: 100%;
}
.metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;