feat: add copyable cli test command
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user