更新若干功能
82
tools/applyinit/main.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Applies init.sql to the local MySQL using the app's built-in default connection.
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "inspect" {
|
||||
inspect()
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile("init.sql")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/?multiStatements=false&charset=utf8mb4")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
// init.sql 的升级段依赖会话变量(SET @sql / PREPARE),必须固定在同一连接上执行。
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
var kept []string
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
if t := strings.TrimSpace(line); strings.HasPrefix(t, "--") {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, line)
|
||||
}
|
||||
for _, stmt := range strings.Split(strings.Join(kept, "\n"), ";") {
|
||||
s := strings.TrimSpace(stmt)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(s); err != nil {
|
||||
fmt.Println("ERR:", err, "stmt:", s[:min(80, len(s))])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
var n int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM code_count.sync_settings").Scan(&n); err != nil {
|
||||
fmt.Println("verify failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("ok, sync_settings rows:", n)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func inspect() {
|
||||
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/code_count?charset=utf8mb4")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SELECT user_id, name, LENGTH(value), updated_at FROM sync_settings ORDER BY user_id, name")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var uid int64
|
||||
var name, at string
|
||||
var n int
|
||||
if err := rows.Scan(&uid, &name, &n, &at); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("user=%d name=%-12s bytes=%-6d at=%s\n", uid, name, n, at)
|
||||
}
|
||||
}
|
||||
119
tools/cdp.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// cdp.js - drive the Code Count WebView2 via Chrome DevTools Protocol.
|
||||
// Usage:
|
||||
// node cdp.js eval "<expression>" evaluate JS (await-ed, JSON result)
|
||||
// node cdp.js click <x> <y> trusted click at CSS viewport coords
|
||||
// node cdp.js rclick <x> <y> trusted right-click
|
||||
// node cdp.js key <Key> [text] dispatch a key press (e.g. Enter, Escape)
|
||||
// node cdp.js type "<text>" insert text into the focused element
|
||||
// node cdp.js shot <file.png> capture in-page screenshot
|
||||
// node cdp.js dialog <accept|dismiss> set auto dialog policy for this run (default: report only)
|
||||
// Multiple commands can be chained with ";;" between argument groups.
|
||||
const PORT = process.env.CC_DEVTOOLS_PORT || '9222';
|
||||
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
let ws;
|
||||
let dialogPolicy = null;
|
||||
const dialogEvents = [];
|
||||
|
||||
function send(method, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
|
||||
const targets = await res.json();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('no page target');
|
||||
ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((ok, bad) => { ws.onopen = ok; ws.onerror = bad; });
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { resolve, reject } = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
|
||||
} else if (msg.method === 'Page.javascriptDialogOpening') {
|
||||
dialogEvents.push(msg.params);
|
||||
console.log('DIALOG-OPEN', JSON.stringify({ type: msg.params.type, message: msg.params.message }));
|
||||
if (dialogPolicy) {
|
||||
send('Page.handleJavaScriptDialog', { accept: dialogPolicy === 'accept' })
|
||||
.then(() => console.log('DIALOG-HANDLED', dialogPolicy))
|
||||
.catch(e => console.log('DIALOG-ERR', e.message));
|
||||
}
|
||||
}
|
||||
};
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
}
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
async function mouse(type, x, y, button, clickCount = 1) {
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none' });
|
||||
await sleep(40);
|
||||
await send('Input.dispatchMouseEvent', { type, x, y, button, clickCount });
|
||||
}
|
||||
|
||||
async function click(x, y, button = 'left') {
|
||||
await mouse('mousePressed', x, y, button);
|
||||
await sleep(50);
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button, clickCount: 1 });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const groups = process.argv.slice(2).join('\u0000').split('\u0000;;\u0000').map(g => g.split('\u0000'));
|
||||
await connect();
|
||||
for (const g of groups) {
|
||||
const [cmd, ...args] = g;
|
||||
if (cmd === 'dialog') {
|
||||
dialogPolicy = args[0];
|
||||
console.log('dialog policy:', dialogPolicy);
|
||||
} else if (cmd === 'eval') {
|
||||
const r = await send('Runtime.evaluate', { expression: args.join(' '), awaitPromise: true, returnByValue: true });
|
||||
console.log('EVAL', JSON.stringify(r.result && 'value' in r.result ? r.result.value : r.result));
|
||||
if (r.exceptionDetails) console.log('EXC', JSON.stringify(r.exceptionDetails.exception?.description || r.exceptionDetails.text));
|
||||
} else if (cmd === 'click') {
|
||||
await click(Number(args[0]), Number(args[1]));
|
||||
console.log('clicked', args[0], args[1]);
|
||||
} else if (cmd === 'rclick') {
|
||||
await click(Number(args[0]), Number(args[1]), 'right');
|
||||
console.log('right-clicked', args[0], args[1]);
|
||||
} else if (cmd === 'key') {
|
||||
const key = args[0];
|
||||
const defs = { Enter: { code: 'Enter', keyCode: 13, text: '\r' }, Escape: { code: 'Escape', keyCode: 27 }, Tab: { code: 'Tab', keyCode: 9 } };
|
||||
const d = defs[key] || { code: key, keyCode: key.charCodeAt(0) };
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyDown', key, code: d.code, windowsVirtualKeyCode: d.keyCode, text: d.text });
|
||||
await sleep(30);
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyUp', key, code: d.code, windowsVirtualKeyCode: d.keyCode });
|
||||
console.log('key', key);
|
||||
} else if (cmd === 'type') {
|
||||
await send('Input.insertText', { text: args.join(' ') });
|
||||
console.log('typed', args.join(' '));
|
||||
} else if (cmd === 'shot') {
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
require('fs').writeFileSync(args[0], Buffer.from(shot.data, 'base64'));
|
||||
console.log('saved', args[0]);
|
||||
} else if (cmd === 'viewport') {
|
||||
// viewport <w> <h> 模拟视口尺寸;viewport 0 恢复真实尺寸
|
||||
const w = Number(args[0]);
|
||||
if (w) await send('Emulation.setDeviceMetricsOverride', { width: w, height: Number(args[1] || 700), deviceScaleFactor: 0, mobile: false });
|
||||
else await send('Emulation.clearDeviceMetricsOverride');
|
||||
console.log('viewport', args.join('x') || 'cleared');
|
||||
} else if (cmd === 'wait') {
|
||||
await sleep(Number(args[0] || 500));
|
||||
} else {
|
||||
throw new Error('unknown cmd: ' + cmd);
|
||||
}
|
||||
await sleep(120);
|
||||
}
|
||||
// give async dialog handlers a moment before exit
|
||||
await sleep(300);
|
||||
ws.close();
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
||||
175
tools/drive.ps1
Normal file
@@ -0,0 +1,175 @@
|
||||
# drive.ps1 - focus the Code Count window, optionally send keys, optionally capture a screenshot.
|
||||
# Usage: powershell -File drive.ps1 -Keys "alt right right down enter" -Out shot.png -SettleMs 800
|
||||
param(
|
||||
[string]$Keys = "",
|
||||
[string]$Out = "",
|
||||
[string]$RClick = "", # "x,y" window-relative right click
|
||||
[string]$LClick = "", # "x,y" window-relative left click
|
||||
[string]$Move = "", # "x,y" window-relative hover (no click)
|
||||
[string]$Wheel = "", # "x,y,ticks" wheel at point (positive ticks = up)
|
||||
[int]$SettleMs = 700,
|
||||
[int]$KeyGapMs = 150,
|
||||
[long]$Hwnd = 0
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -ReferencedAssemblies System.Drawing @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class Drv {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr dc, uint flags);
|
||||
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
||||
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint fl, UIntPtr ex);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
|
||||
[DllImport("user32.dll")] public static extern void mouse_event(uint fl, int dx, int dy, uint data, UIntPtr ex);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls, string title);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr h, System.Text.StringBuilder sb, int n);
|
||||
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L; public int T; public int R; public int B; }
|
||||
}
|
||||
"@
|
||||
|
||||
[Drv]::SetProcessDPIAware() | Out-Null
|
||||
if ($Hwnd -ne 0) {
|
||||
$h = [IntPtr]$Hwnd
|
||||
} else {
|
||||
# Window title is non-ASCII now; resolve via process main window handle to keep this file ASCII-safe.
|
||||
$proc = Get-Process code-count | Where-Object { $_.MainWindowHandle -ne 0 } | Sort-Object StartTime -Descending | Select-Object -First 1
|
||||
if (-not $proc) { throw "app window not found" }
|
||||
$h = $proc.MainWindowHandle
|
||||
}
|
||||
if ([Drv]::IsIconic($h)) {
|
||||
[Drv]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
|
||||
Start-Sleep -Milliseconds 600
|
||||
Write-Output "restored from minimized"
|
||||
}
|
||||
[Drv]::SetForegroundWindow($h) | Out-Null
|
||||
Start-Sleep -Milliseconds 300
|
||||
if ([Drv]::GetForegroundWindow() -ne $h) {
|
||||
# press Alt to satisfy the foreground-lock heuristic, then retry
|
||||
[Drv]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
|
||||
[Drv]::SetForegroundWindow($h) | Out-Null
|
||||
[Drv]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
Write-Output ("foreground=" + ([Drv]::GetForegroundWindow() -eq $h))
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
$vk = @{ alt=0x12; ctrl=0x11; shift=0x10; enter=0x0D; esc=0x1B; tab=0x09; space=0x20; f10=0x79;
|
||||
left=0x25; up=0x26; right=0x27; down=0x28; apps=0x5D }
|
||||
$ext = @(0x25,0x26,0x27,0x28,0x5D)
|
||||
|
||||
function Press([int[]]$codes) {
|
||||
foreach ($c in $codes) {
|
||||
$f = if ($ext -contains $c) { 1 } else { 0 }
|
||||
[Drv]::keybd_event([byte]$c, 0, [uint32]$f, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 25
|
||||
}
|
||||
[array]::Reverse($codes)
|
||||
foreach ($c in $codes) {
|
||||
$f = if ($ext -contains $c) { 3 } else { 2 }
|
||||
[Drv]::keybd_event([byte]$c, 0, [uint32]$f, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 25
|
||||
}
|
||||
}
|
||||
|
||||
if ($Keys) {
|
||||
foreach ($tok in $Keys.Split(' ')) {
|
||||
if (-not $tok) { continue }
|
||||
if ($tok.StartsWith('type:')) {
|
||||
foreach ($ch in $tok.Substring(5).ToCharArray()) { Press @([int][char]::ToUpper($ch)) }
|
||||
continue
|
||||
}
|
||||
$codes = @()
|
||||
foreach ($part in $tok.Split('+')) {
|
||||
if ($vk.ContainsKey($part)) { $codes += [int]$vk[$part] }
|
||||
elseif ($part.Length -eq 1) { $codes += [int][char]::ToUpper($part[0]) }
|
||||
else { throw "unknown key: $part" }
|
||||
}
|
||||
Press $codes
|
||||
Start-Sleep -Milliseconds $KeyGapMs
|
||||
}
|
||||
}
|
||||
|
||||
if ($Move) {
|
||||
$xy = $Move.Split(',')
|
||||
$rm = New-Object Drv+RECT
|
||||
[Drv]::GetWindowRect($h, [ref]$rm) | Out-Null
|
||||
[Drv]::SetCursorPos($rm.L + [int]$xy[0], $rm.T + [int]$xy[1]) | Out-Null
|
||||
# nudge one pixel so the webview registers a mousemove event
|
||||
Start-Sleep -Milliseconds 80
|
||||
[Drv]::mouse_event(0x0001, 1, 0, 0, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
|
||||
if ($RClick) {
|
||||
$xy = $RClick.Split(',')
|
||||
$r0 = New-Object Drv+RECT
|
||||
[Drv]::GetWindowRect($h, [ref]$r0) | Out-Null
|
||||
$px = $r0.L + [int]$xy[0]; $py = $r0.T + [int]$xy[1]
|
||||
[Drv]::SetCursorPos($px, $py) | Out-Null
|
||||
Start-Sleep -Milliseconds 150
|
||||
[Drv]::mouse_event(0x0008, 0, 0, 0, [UIntPtr]::Zero) # right down
|
||||
Start-Sleep -Milliseconds 60
|
||||
[Drv]::mouse_event(0x0010, 0, 0, 0, [UIntPtr]::Zero) # right up
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
|
||||
if ($LClick) {
|
||||
$xy = $LClick.Split(',')
|
||||
$r1 = New-Object Drv+RECT
|
||||
[Drv]::GetWindowRect($h, [ref]$r1) | Out-Null
|
||||
$tx = $r1.L + [int]$xy[0]; $ty = $r1.T + [int]$xy[1]
|
||||
[Drv]::SetCursorPos($tx - 1, $ty) | Out-Null
|
||||
Start-Sleep -Milliseconds 80
|
||||
[Drv]::mouse_event(0x0001, 1, 0, 0, [UIntPtr]::Zero) # real WM_MOUSEMOVE for the webview
|
||||
Start-Sleep -Milliseconds 120
|
||||
$cp = New-Object Drv+POINT
|
||||
[Drv]::GetCursorPos([ref]$cp) | Out-Null
|
||||
$wfp = [Drv]::WindowFromPoint($cp)
|
||||
$wpid = 0
|
||||
[Drv]::GetWindowThreadProcessId($wfp, [ref]$wpid) | Out-Null
|
||||
$wcls = New-Object System.Text.StringBuilder 128
|
||||
[Drv]::GetClassName($wfp, $wcls, 128) | Out-Null
|
||||
Write-Output "rect=($($r1.L),$($r1.T))-($($r1.R),$($r1.B)) target=($tx,$ty) cursor=($($cp.X),$($cp.Y)) under=$wfp cls=$($wcls.ToString()) pid=$wpid"
|
||||
[Drv]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero) # left down
|
||||
Start-Sleep -Milliseconds 60
|
||||
[Drv]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero) # left up
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
|
||||
if ($Wheel) {
|
||||
$wp = $Wheel.Split(',')
|
||||
$r2 = New-Object Drv+RECT
|
||||
[Drv]::GetWindowRect($h, [ref]$r2) | Out-Null
|
||||
[Drv]::SetCursorPos($r2.L + [int]$wp[0], $r2.T + [int]$wp[1]) | Out-Null
|
||||
Start-Sleep -Milliseconds 120
|
||||
$ticks = [int]$wp[2]
|
||||
$dir = if ($ticks -gt 0) { 120 } else { -120 }
|
||||
for ($i = 0; $i -lt [Math]::Abs($ticks); $i++) {
|
||||
[Drv]::mouse_event(0x0800, 0, 0, [uint32]($dir -band 0xFFFFFFFF), [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 60
|
||||
}
|
||||
}
|
||||
|
||||
if ($Out) {
|
||||
Start-Sleep -Milliseconds $SettleMs
|
||||
$r = New-Object Drv+RECT
|
||||
[Drv]::GetWindowRect($h, [ref]$r) | Out-Null
|
||||
$w = $r.R - $r.L; $ht = $r.B - $r.T
|
||||
$bmp = New-Object System.Drawing.Bitmap($w, $ht)
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$dc = $g.GetHdc()
|
||||
[Drv]::PrintWindow($h, $dc, 2) | Out-Null
|
||||
$g.ReleaseHdc($dc); $g.Dispose()
|
||||
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$bmp.Dispose()
|
||||
Write-Output "saved $Out ($w x $ht)"
|
||||
}
|
||||
62
tools/dumpmeta/main.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// dumpmeta:读取本地 SQLite 的 sync_* 配置,排查同步实际连接的 MySQL 目标。
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dir, _ := os.UserConfigDir()
|
||||
path := filepath.Join(dir, "CodeCount", "code-count.db")
|
||||
if len(os.Args) > 1 {
|
||||
path = os.Args[1]
|
||||
}
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
rows, err := db.Query(`SELECT key,value FROM settings WHERE key LIKE 'sync_%' ORDER BY key`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var k, v string
|
||||
_ = rows.Scan(&k, &v)
|
||||
if k == "sync_password" || k == "sync_enc_key" {
|
||||
v = fmt.Sprintf("<len %d>", len(v))
|
||||
}
|
||||
fmt.Printf("%-22s = %s\n", k, v)
|
||||
}
|
||||
fmt.Println("---- festival_images ----")
|
||||
fr, err := db.Query(`SELECT fest_key,LENGTH(value),updated_at,dirty FROM festival_images ORDER BY fest_key`)
|
||||
if err == nil {
|
||||
for fr.Next() {
|
||||
var k, at string
|
||||
var n, dirty int
|
||||
_ = fr.Scan(&k, &n, &at, &dirty)
|
||||
fmt.Printf("%-10s bytes=%-7d at=%s dirty=%d\n", k, n, at, dirty)
|
||||
}
|
||||
fr.Close()
|
||||
} else {
|
||||
fmt.Println("(no table)", err)
|
||||
}
|
||||
fmt.Println("---- recent warning/error logs ----")
|
||||
lr, err := db.Query(`SELECT id,level,message,detail,created_at FROM app_logs WHERE level IN('warning','error') ORDER BY id DESC LIMIT 12`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer lr.Close()
|
||||
for lr.Next() {
|
||||
var id int64
|
||||
var level, msg, detail, at string
|
||||
_ = lr.Scan(&id, &level, &msg, &detail, &at)
|
||||
fmt.Printf("#%d [%s] %s | %s | %s\n", id, level, at, msg, detail)
|
||||
}
|
||||
}
|
||||
BIN
tools/exe-icon-check.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
tools/exe-icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
6
tools/extract-icon.ps1
Normal file
@@ -0,0 +1,6 @@
|
||||
# 从 exe 提取关联图标存为 png,用于验证 syso 图标是否打包成功
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$exe = 'D:\myCode\code-count\view\bin\code-count.exe'
|
||||
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($exe)
|
||||
$icon.ToBitmap().Save('D:\myCode\code-count\view\tools\exe-icon-check.png', [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
Write-Output ("exe size: " + (Get-Item $exe).Length)
|
||||
6
tools/launch-cdp.ps1
Normal file
@@ -0,0 +1,6 @@
|
||||
Get-Process code-count -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Milliseconds 800
|
||||
$env:CC_DEVTOOLS_PORT = '9222'
|
||||
Start-Process -FilePath 'd:\myCode\code-count\view\bin\code-count.exe'
|
||||
Start-Sleep -Seconds 6
|
||||
(Invoke-WebRequest -Uri 'http://127.0.0.1:9222/json/list' -UseBasicParsing).Content
|
||||
BIN
tools/shot-about-modal.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
tools/shot-about-modal2.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
tools/shot-about.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
tools/shot-about2.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
tools/shot-about3.png
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
tools/shot-about4.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
tools/shot-ai-brief.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
tools/shot-ai-brief2.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
tools/shot-ai-brief3.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
tools/shot-ai-day.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
tools/shot-ai-drawer.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
tools/shot-ai-grid.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
tools/shot-ai-layout.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
tools/shot-ai-masonry.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
tools/shot-ai-tab.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
tools/shot-ask-ai.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
tools/shot-avatar-modal.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
tools/shot-brand-restore.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
tools/shot-final-home.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
tools/shot-gap.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
tools/shot-git-clone.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
tools/shot-launchpad.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
tools/shot-lc-cal.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
tools/shot-lc-todo.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
tools/shot-lc-todo2.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
tools/shot-lp-modal.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
tools/shot-narrow-flyout.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
tools/shot-nav2col.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
tools/shot-note-center.png
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
tools/shot-note-modal.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
tools/shot-profile-c1.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
tools/shot-profile-c2.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
tools/shot-profile-c3.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
tools/shot-profile-center.png
Normal file
|
After Width: | Height: | Size: 357 KiB |
BIN
tools/shot-profile-entry.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
tools/shot-profile-tabs.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
tools/shot-taskcenter.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
tools/shot-tc-modal.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
1
tools/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
年糕崽崽项目管理(PMS) · 奶酪云
|
||||
BIN
tools/verify-cal.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
tools/verify-cal2.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
tools/verify-home.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
103
tools/verify-md.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// verify-md.js - end-to-end check of markdown rendering in todos/tickets via CDP.
|
||||
// Usage: node verify-md.js <stage> stages: todo-editor | preview | save | ticket | settings | cleanup
|
||||
const PORT = process.env.CC_DEVTOOLS_PORT || '9222';
|
||||
let seq = 0; const pending = new Map(); let ws;
|
||||
|
||||
function send(method, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++seq; pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
}
|
||||
async function connect() {
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
|
||||
const page = (await res.json()).find(t => t.type === 'page');
|
||||
ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((ok, bad) => { ws.onopen = ok; ws.onerror = bad; });
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { resolve, reject } = pending.get(msg.id); pending.delete(msg.id);
|
||||
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
|
||||
} else if (msg.method === 'Page.javascriptDialogOpening') {
|
||||
send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
|
||||
}
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
}
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function evalIn(expression) {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result?.value;
|
||||
}
|
||||
const setInput = (sel, val) => `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el.value = ${JSON.stringify(val)}; el.dispatchEvent(new Event('input', { bubbles: true })); return el.value.length })()`;
|
||||
const setSelect = (sel, expr) => `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el.value = ${expr}; el.dispatchEvent(new Event('change', { bubbles: true })); return el.value })()`;
|
||||
|
||||
const TODO_TITLE = 'MD渲染验证-待办';
|
||||
const TICKET_TITLE = 'MD渲染验证-工单';
|
||||
// canvas 生成一张可见的渐变图,模拟 base64 模式产物;再引用一张真实存在的本地路径图。
|
||||
const CANVAS_IMG = `(() => { const c = document.createElement('canvas'); c.width = 200; c.height = 100; const g = c.getContext('2d'); const gr = g.createLinearGradient(0, 0, 200, 100); gr.addColorStop(0, '#7367f5'); gr.addColorStop(1, '#43c996'); g.fillStyle = gr; g.fillRect(0, 0, 200, 100); g.fillStyle = '#fff'; g.font = 'bold 22px sans-serif'; g.fillText('IMG', 74, 58); return c.toDataURL('image/png') })()`;
|
||||
const LOCAL_IMG = 'D:/myCode/code-count/view/tools/shot-todos.png';
|
||||
|
||||
function mdBody(dataURL) {
|
||||
return ['### 修复登录页样式问题', '', '- [x] 复现并定位 `z-index` 冲突', '- [ ] **回归测试**全部表单', '', '> 参考 [MDN 文档](https://developer.mozilla.org/) 的层叠上下文说明', '',
|
||||
'```css', '.topbar{backdrop-filter:blur(28px)}', '```', '', ``, '', ``].join('\n');
|
||||
}
|
||||
|
||||
const stages = {
|
||||
async 'todo-editor'() {
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(600);
|
||||
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(400);
|
||||
console.log('title-len', await evalIn(setInput('.modal input', TODO_TITLE)));
|
||||
const dataURL = await evalIn(CANVAS_IMG);
|
||||
console.log('content-len', await evalIn(setInput('.md-editor textarea', mdBody(dataURL))));
|
||||
await sleep(300);
|
||||
console.log('toolbar', await evalIn(`document.querySelectorAll('.md-toolbar .tabs button').length`));
|
||||
},
|
||||
async preview() {
|
||||
await evalIn(`document.querySelectorAll('.md-toolbar .tabs button')[1].click()`); await sleep(900);
|
||||
console.log('preview-imgs', await evalIn(`[...document.querySelectorAll('.md-preview-box img')].map(i => i.complete && i.naturalWidth > 0)`));
|
||||
console.log('preview-has', await evalIn(`(() => { const b = document.querySelector('.md-preview-box'); return { h3: !!b.querySelector('h3'), code: !!b.querySelector('pre code'), quote: !!b.querySelector('blockquote'), check: b.querySelectorAll('input[type=checkbox]').length } })()`));
|
||||
},
|
||||
async save() {
|
||||
await evalIn(`document.querySelector('.modal footer .btn.primary').click()`); await sleep(1500);
|
||||
console.log('card', await evalIn(`(() => { const card = [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(${JSON.stringify(TODO_TITLE)})); if (!card) return 'NOT_FOUND'; const md = card.querySelector('.md-content'); return { imgs: [...md.querySelectorAll('img')].map(i => i.complete && i.naturalWidth > 0), h3: !!md.querySelector('h3'), clamp: md.classList.contains('md-clamp') } })()`));
|
||||
},
|
||||
async ticket() {
|
||||
await evalIn(`location.hash = '#/tickets'`); await sleep(600);
|
||||
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(400);
|
||||
console.log('title-len', await evalIn(setInput('.modal input', TICKET_TITLE)));
|
||||
const dataURL = await evalIn(CANVAS_IMG);
|
||||
console.log('desc-len', await evalIn(setInput('.md-editor textarea', '**必须**本周内完成,涉及 `sync.go`\n\n')));
|
||||
console.log('project', await evalIn(setSelect('.modal-grid label:nth-child(2) select', `[...document.querySelectorAll('.modal-grid label:nth-child(2) select option')].find(o => o.value !== '0').value`)));
|
||||
console.log('due', await evalIn(setInput('.modal-grid input[type=date]:nth-of-type(1)', '2026-08-20')));
|
||||
await evalIn(`(() => { const due = [...document.querySelectorAll('.modal-grid input[type=date]')][1]; due.value = '2026-08-20'; due.dispatchEvent(new Event('input', { bubbles: true })) })()`);
|
||||
await sleep(200);
|
||||
await evalIn(`document.querySelector('.modal footer .btn.primary').click()`); await sleep(1500);
|
||||
console.log('row', await evalIn(`(() => { const row = [...document.querySelectorAll('.ticket-row')].find(r => r.querySelector('b')?.textContent.includes(${JSON.stringify(TICKET_TITLE)})); if (!row) return 'NOT_FOUND'; const md = row.querySelector('.md-content'); return { img: !!md.querySelector('img'), bold: !!md.querySelector('strong'), code: !!md.querySelector('code') } })()`));
|
||||
},
|
||||
async settings() {
|
||||
// 图片存储设置已随账号资料迁至个人主页
|
||||
await evalIn(`location.hash = '#/profile'`); await sleep(800);
|
||||
console.log('img-mode', await evalIn(`(() => { const labels = [...document.querySelectorAll('.form-panel label')]; const l = labels.find(x => x.textContent.includes('内容图片存储')); if (!l) return 'NOT_FOUND'; l.scrollIntoView({ block: 'center' }); const s = l.querySelector('select'); return { value: s.value, options: [...s.options].map(o => o.value) } })()`));
|
||||
await sleep(400);
|
||||
},
|
||||
async cleanup() {
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(600);
|
||||
await evalIn(`(() => { const card = [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(${JSON.stringify(TODO_TITLE)})); card?.querySelector('.todo-remove')?.click() })()`);
|
||||
await sleep(900);
|
||||
await evalIn(`location.hash = '#/tickets'`); await sleep(600);
|
||||
await evalIn(`(() => { const row = [...document.querySelectorAll('.ticket-row')].find(r => r.querySelector('b')?.textContent.includes(${JSON.stringify(TICKET_TITLE)})); row?.querySelector('.icon-actions button:last-child')?.click() })()`);
|
||||
await sleep(900);
|
||||
console.log('left', await evalIn(`[...document.querySelectorAll('.ticket-row b')].filter(b => b.textContent.includes('MD渲染验证')).length`));
|
||||
},
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await connect();
|
||||
const stage = process.argv[2];
|
||||
if (!stages[stage]) throw new Error('unknown stage: ' + stage);
|
||||
await stages[stage]();
|
||||
await sleep(200); ws.close();
|
||||
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
||||
BIN
tools/verify-notes.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
300
tools/verify-quick.js
Normal file
@@ -0,0 +1,300 @@
|
||||
// verify-quick.js - check markdown editor inside calendar quick-create modal via CDP.
|
||||
// Usage: node verify-quick.js <stage> stages: todo | preview | create | ticket | cleanup
|
||||
const PORT = process.env.CC_DEVTOOLS_PORT || '9222';
|
||||
let seq = 0; const pending = new Map(); let ws;
|
||||
|
||||
function send(method, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++seq; pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
}
|
||||
async function connect() {
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
|
||||
const page = (await res.json()).find(t => t.type === 'page');
|
||||
ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((ok, bad) => { ws.onopen = ok; ws.onerror = bad; });
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { resolve, reject } = pending.get(msg.id); pending.delete(msg.id);
|
||||
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
|
||||
} else if (msg.method === 'Page.javascriptDialogOpening') {
|
||||
send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
|
||||
}
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
}
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function evalIn(expression) {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result?.value;
|
||||
}
|
||||
async function shot(file) {
|
||||
const s = await send('Page.captureScreenshot', { format: 'png' });
|
||||
require('fs').writeFileSync(file, Buffer.from(s.data, 'base64'));
|
||||
console.log('saved', file);
|
||||
}
|
||||
const setInput = (sel, val) => `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el.value = ${JSON.stringify(val)}; el.dispatchEvent(new Event('input', { bubbles: true })); return el.value.length })()`;
|
||||
|
||||
const TODO_TITLE = '快速创建MD-待办';
|
||||
const MD = ['**重点**:联调 `SaveTodo` 接口', '', '- [x] 标题输入', '- [ ] 图片粘贴', '', '> 日历右键快速创建也支持 Markdown 了'].join('\n');
|
||||
|
||||
async function openQuickFromCtx(buttonIdx) {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(700);
|
||||
await evalIn(`(() => { const el = document.querySelector('.calendar-cell.today') || document.querySelectorAll('.calendar-cell')[17]; const r = el.getBoundingClientRect(); el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: r.x + 40, clientY: r.y + 30 })); return 'ctx' })()`);
|
||||
await sleep(400);
|
||||
await evalIn(`document.querySelectorAll('.calendar-ctx button')[${buttonIdx}].click()`);
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
const stages = {
|
||||
async todo() {
|
||||
await openQuickFromCtx(0);
|
||||
// 快速创建待办/工单已与待办页对齐:同为 modal-split 分栏布局
|
||||
console.log('editor', await evalIn(`(() => { const m = document.querySelector('.modal-split'); if (!m) return 'NO_MODAL'; return { editor: !!m.querySelector('.split-editor .md-editor'), tabs: m.querySelectorAll('.md-toolbar .tabs button').length, imgBtn: !!m.querySelector('.md-img-btn'), fields: [...m.querySelectorAll('.split-fields > label, .split-fields .field-pair label')].map(l => l.firstChild.textContent.trim()), duePick: !!m.querySelector('.due-quick'), date: m.querySelector('.quick-date')?.textContent } })()`));
|
||||
console.log('title-len', await evalIn(setInput('.modal-split .split-fields input', TODO_TITLE)));
|
||||
console.log('content-len', await evalIn(setInput('.modal-split .md-editor textarea', MD)));
|
||||
await sleep(300);
|
||||
await shot('tools/shot-quick-edit.png');
|
||||
},
|
||||
async preview() {
|
||||
await evalIn(`document.querySelectorAll('.modal-split .md-toolbar .tabs button')[1].click()`); await sleep(700);
|
||||
console.log('preview', await evalIn(`(() => { const b = document.querySelector('.modal-split .md-preview-box'); return { bold: !!b.querySelector('strong'), code: !!b.querySelector('code'), quote: !!b.querySelector('blockquote'), checks: b.querySelectorAll('input[type=checkbox]').length } })()`));
|
||||
await shot('tools/shot-quick-preview.png');
|
||||
},
|
||||
async create() {
|
||||
await evalIn(`(() => { const btns = [...document.querySelectorAll('.modal-split footer .btn.primary')]; btns[btns.length - 1].click(); return 'clicked' })()`);
|
||||
await sleep(1200);
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(800);
|
||||
// 视图模式被 localStorage 记忆,先切回看板再断言卡片与 MD 渲染
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.view-toggle button')][0]?.click(); return 'board' })()`); await sleep(400);
|
||||
console.log('card', await evalIn(`(() => { const card = [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(${JSON.stringify(TODO_TITLE)})); if (!card) return 'NOT_FOUND'; const md = card.querySelector('.md-content'); return { md: !!md, bold: !!md?.querySelector('strong'), quote: !!md?.querySelector('blockquote') } })()`));
|
||||
await shot('tools/shot-quick-card.png');
|
||||
},
|
||||
async ticket() {
|
||||
await openQuickFromCtx(1);
|
||||
console.log('ticket-editor', await evalIn(`(() => { const m = document.querySelector('.modal-split'); if (!m) return 'NO_MODAL'; return { editor: !!m.querySelector('.split-editor .md-editor'), fields: [...m.querySelectorAll('.split-fields > label')].map(l => l.firstChild.textContent.trim()), duePick: !!m.querySelector('.due-quick'), projectRequired: m.querySelector('.split-fields select')?.hasAttribute('required') } })()`));
|
||||
await shot('tools/shot-quick-ticket.png');
|
||||
await evalIn(`document.querySelector('.modal-split header button').click()`);
|
||||
},
|
||||
async reminder() {
|
||||
await openQuickFromCtx(2);
|
||||
console.log('reminder-modal', await evalIn(`(() => { const m = document.querySelector('.quick-modal'); if (!m) return 'NO_MODAL'; return { compact: true, time: !!m.querySelector('input[type=time]'), noEditor: !m.querySelector('.md-editor') } })()`));
|
||||
await evalIn(`document.querySelector('.quick-modal .login-close').click()`);
|
||||
},
|
||||
async 'ticket-modal'() {
|
||||
await evalIn(`document.activeElement?.blur?.(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))`);
|
||||
await evalIn(`location.hash = '#/tickets'`); await sleep(700);
|
||||
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(500);
|
||||
console.log('title-len', await evalIn(setInput('.split-fields input', '登录页样式回归')));
|
||||
console.log('desc-len', await evalIn(setInput('.md-editor textarea', ['**必须**本周完成,涉及 `sync.go`', '', '- [ ] 联调', '- [x] 自测', '', '> 注意灰度发布窗口'].join('\n'))));
|
||||
await sleep(300);
|
||||
await shot('tools/shot-split-ticket-edit.png');
|
||||
await evalIn(`document.querySelectorAll('.md-toolbar .tabs button')[1].click()`); await sleep(600);
|
||||
await shot('tools/shot-split-ticket-preview.png');
|
||||
await evalIn(`document.querySelector('.modal header button').click()`);
|
||||
},
|
||||
async 'calendar-egg'() {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(800);
|
||||
console.log('eggs', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.has-egg')]; return cells.map(c => { const bg = getComputedStyle(c, '::before').backgroundImage; return { day: c.querySelector('.calendar-day').textContent, fest: c.querySelector('.calendar-fest')?.textContent, svg: bg.startsWith('url("data:image/svg+xml'), emoji: /%F0%9F|\\uD83D/.test(bg) } }) })()`));
|
||||
// 选中一个节日格,检查详情面板的矢量图标
|
||||
await evalIn(`document.querySelector('.calendar-cell.has-egg')?.click()`); await sleep(400);
|
||||
console.log('detail-vec', await evalIn(`(() => { const v = document.querySelector('.calendar-egg-vec'); if (!v) return 'NO_VEC'; return { svg: getComputedStyle(v).backgroundImage.startsWith('url("data:image/svg+xml'), size: getComputedStyle(v).width } })()`));
|
||||
await shot('tools/shot-egg-bg.png');
|
||||
},
|
||||
async 'daily-entry'() {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(800);
|
||||
console.log('entries', await evalIn(`(() => ({ headBtn: !!document.querySelector('.daily-open-btn'), panelBtn: !!document.querySelector('.daily-entry'), panelDisabled: document.querySelector('.daily-entry')?.disabled }))()`));
|
||||
// 未来日期入口应禁用
|
||||
await evalIn(`(() => { const today = new Date().getDate(); const c = [...document.querySelectorAll('.calendar-cell:not(.out)')].find(x => parseInt(x.querySelector('.calendar-day').textContent) === Math.min(today + 5, 28)); c?.click(); return 'future' })()`); await sleep(350);
|
||||
console.log('future-disabled', await evalIn(`document.querySelector('.daily-entry')?.disabled`));
|
||||
// 回到今天并打开卡片
|
||||
await evalIn(`document.querySelector('.calendar-cell.today')?.click()`); await sleep(300);
|
||||
const before = await evalIn(`localStorage.getItem('cc-daily-card')`);
|
||||
await evalIn(`document.querySelector('.daily-entry').click()`); await sleep(1600);
|
||||
console.log('opened', await evalIn(`(() => { const c = document.querySelector('.daily-card'); if (!c) return 'NO_CARD'; return { date: c.querySelector('.daily-date')?.textContent, history: !!c.querySelector('.daily-history-tag'), nav: document.querySelectorAll('.daily-nav').length, nextDisabled: document.querySelector('.daily-nav.next')?.disabled } })()`));
|
||||
await shot('tools/shot-daily-entry-today.png');
|
||||
// 翻到前一天:历史标签出现,后一天按钮可用
|
||||
await evalIn(`document.querySelector('.daily-nav.prev').click()`); await sleep(1600);
|
||||
console.log('prev-day', await evalIn(`(() => { const c = document.querySelector('.daily-card'); return { date: c.querySelector('.daily-date')?.textContent, history: !!c.querySelector('.daily-history-tag'), nextDisabled: document.querySelector('.daily-nav.next')?.disabled, quote: c.querySelector('.daily-quote')?.textContent.slice(0, 18) } })()`));
|
||||
await shot('tools/shot-daily-history.png');
|
||||
// 翻回今天:历史标签消失、后一天禁用;手动打开关闭后不写入已读标记
|
||||
await evalIn(`document.querySelector('.daily-nav.next').click()`); await sleep(900);
|
||||
console.log('back-today', await evalIn(`(() => ({ history: !!document.querySelector('.daily-history-tag'), nextDisabled: document.querySelector('.daily-nav.next')?.disabled }))()`));
|
||||
await evalIn(`document.querySelector('.daily-close').click()`); await sleep(400);
|
||||
console.log('closed', { stampUnchanged: (await evalIn(`localStorage.getItem('cc-daily-card')`)) === before, gone: await evalIn(`!document.querySelector('.daily-card')`) });
|
||||
},
|
||||
async 'calendar-cross'() {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(600);
|
||||
console.log('before', await evalIn(`document.querySelector('.calendar-month').textContent`));
|
||||
console.log('clicked-out-date', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.out')]; const c = cells[cells.length - 1]; const d = c.querySelector('.calendar-day').textContent; c.click(); return d })()`));
|
||||
await sleep(600);
|
||||
console.log('after', await evalIn(`document.querySelector('.calendar-month').textContent`));
|
||||
console.log('selected-in-month', await evalIn(`(() => { const s = document.querySelector('.calendar-cell.selected'); return s ? { day: s.querySelector('.calendar-day').textContent, out: s.classList.contains('out') } : 'NONE' })()`));
|
||||
await shot('tools/shot-cross-month.png');
|
||||
},
|
||||
async 'due-quick'() {
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(700);
|
||||
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(500);
|
||||
console.log('chips', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
|
||||
// 点击「3天后」,断言 dueAt 被填充
|
||||
await evalIn(`[...document.querySelectorAll('.dq-chip')].find(b => b.textContent.includes('3'))?.click()`); await sleep(300);
|
||||
console.log('picked', await evalIn(`(() => { const v = document.querySelector('.split-fields input[type=datetime-local]').value; const d = new Date(); d.setDate(d.getDate() + 3); const exp = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + 'T18:00'; return { v, exp, ok: v === exp } })()`));
|
||||
// 添加自定义 5 天标签
|
||||
await evalIn(`document.querySelector('.dq-add').click()`); await sleep(250);
|
||||
await evalIn(`(() => { const i = document.querySelector('.dq-input input'); i.value = '5'; i.dispatchEvent(new Event('input', { bubbles: true })); i.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })); return 'added' })()`); await sleep(300);
|
||||
console.log('after-add', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
|
||||
await shot('tools/shot-due-quick.png');
|
||||
// 删除「7天后」标签
|
||||
await evalIn(`(() => { const chip = [...document.querySelectorAll('.dq-chip')].find(b => b.textContent.includes('7')); chip.querySelector('.dq-x').dispatchEvent(new MouseEvent('click', { bubbles: true })); return 'removed' })()`); await sleep(300);
|
||||
console.log('after-del', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
|
||||
console.log('persisted', await evalIn(`localStorage.getItem('cc-due-quick')`));
|
||||
await evalIn(`document.querySelector('.modal header button').click()`);
|
||||
},
|
||||
async 'login-shot'() {
|
||||
await evalIn(`(() => { const s = (document.querySelector('#app')?.__vue_app__ || window.app).config.globalProperties.$pinia.state.value.app; s.loginOpen = true; return 'opened' })()`);
|
||||
await sleep(900);
|
||||
console.log('layout', await evalIn(`(() => { const c = document.querySelector('.login-card'); if (!c) return 'NO_CARD'; const r = c.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height), art: !!c.querySelector('.lg-art'), tabs: c.querySelectorAll('.lg-tabs button').length, fields: c.querySelectorAll('.lg-field').length } })()`));
|
||||
await shot('tools/shot-login-v4.png');
|
||||
await evalIn(`document.querySelectorAll('.lg-tabs button')[1].click()`);
|
||||
await sleep(600);
|
||||
await shot('tools/shot-login-v4-reg.png');
|
||||
await evalIn(`document.querySelector('.login-close').click()`);
|
||||
},
|
||||
async daily() {
|
||||
await evalIn(`localStorage.removeItem('cc-daily-card')`);
|
||||
console.log('flip-login', await evalIn(`(() => { const s = (document.querySelector('#app')?.__vue_app__ || window.app).config.globalProperties.$pinia.state.value.app; s.syncStatus.loggedIn = true; return s.syncStatus.loggedIn })()`));
|
||||
await sleep(2600);
|
||||
console.log('card', await evalIn(`(() => { const c = document.querySelector('.daily-card'); if (!c) return 'NO_CARD'; return { date: c.querySelector('.daily-date')?.textContent, sub: c.querySelector('.daily-sub')?.textContent, fest: c.querySelector('.daily-fest')?.textContent || '(none)', quote: c.querySelector('.daily-quote')?.textContent.slice(0, 24), yi: [...c.querySelectorAll('.daily-yi span')].map(x => x.textContent), ji: [...c.querySelectorAll('.daily-ji span')].map(x => x.textContent), img: (() => { const i = c.querySelector('.daily-hero img'); return i ? (i.complete && i.naturalWidth > 0 ? 'loaded' : 'pending') : 'fallback' })() } })()`));
|
||||
await sleep(1800);
|
||||
await shot('tools/shot-daily-card.png');
|
||||
console.log('close', await evalIn(`(() => { document.querySelector('.daily-ok').click(); return { stored: localStorage.getItem('cc-daily-card'), gone: !document.querySelector('.daily-card') } })()`));
|
||||
},
|
||||
async 'fest-img'() {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(1400);
|
||||
// 0) 登录 liqi(云端 id=1),并清掉上次运行可能残留的立秋图,保证幂等
|
||||
const st0 = await evalIn(`__cc.call('GetSyncStatus')`);
|
||||
console.log('login-before', { loggedIn: st0.loggedIn, userId: st0.userId });
|
||||
if (!st0.loggedIn || st0.userId !== 1) {
|
||||
console.log('login-result', await evalIn(`__cc.call('SyncLogin', 'liqi', 'qiqi991012').then(s => ({ loggedIn: s.loggedIn, userId: s.userId })).catch(e => 'ERR:' + e)`));
|
||||
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); return s.refreshSyncStatus() })()`);
|
||||
await sleep(800);
|
||||
}
|
||||
await evalIn(`__cc.call('RemoveFestivalImage', '立秋').catch(() => 'skip')`);
|
||||
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
|
||||
await sleep(600);
|
||||
// 1) 默认插画背景:节日格应有 has-art + svg 背景
|
||||
console.log('default-art', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.has-art')]; return cells.map(c => ({ day: c.querySelector('.calendar-day').textContent, fest: c.querySelector('.calendar-fest')?.textContent, svg: (getComputedStyle(c, '::before').backgroundImage || '').includes('data:image/svg+xml') })) })()`));
|
||||
await shot('tools/shot-fest-art.png');
|
||||
// 3) 选中立秋(8/7),点击头部管理按钮打开样式模态框:双卡选择器,无图时动态卡激活、图片卡显示上传占位
|
||||
await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); c?.click(); return 'sel' })()`); await sleep(400);
|
||||
await evalIn(`(() => { const b = document.querySelector('.fest-admin-btn'); if (!b) return 'NO_BTN'; b.click(); return 'opened' })()`); await sleep(400);
|
||||
console.log('admin-panel', await evalIn(`(() => ({ panel: !!document.querySelector('.fest-admin-modal'), rows: [...document.querySelectorAll('.fest-style-name')].map(x => x.textContent), cards: document.querySelectorAll('.fest-card').length, activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim(), upload: !!document.querySelector('.fest-card-upload') }))()`));
|
||||
await shot('tools/shot-fest-picker-empty.png');
|
||||
// 4) 用 canvas 生成一张"秋天"照片风图片,通过 dataURL 绑定设置
|
||||
console.log('set-img', await evalIn(`(() => {
|
||||
const cv = document.createElement('canvas'); cv.width = 640; cv.height = 420;
|
||||
const g = cv.getContext('2d');
|
||||
const lg = g.createLinearGradient(0, 0, 0, 420); lg.addColorStop(0, '#f2b26b'); lg.addColorStop(.55, '#c96a3b'); lg.addColorStop(1, '#5c2f1e');
|
||||
g.fillStyle = lg; g.fillRect(0, 0, 640, 420);
|
||||
g.fillStyle = 'rgba(255,236,180,.85)'; g.beginPath(); g.arc(500, 90, 46, 0, 7); g.fill();
|
||||
for (let i = 0; i < 26; i++) { g.fillStyle = 'rgba(120,50,20,.' + (3 + i % 5) + ')'; g.beginPath(); g.ellipse(30 + i * 24, 300 + (i % 7) * 14, 9, 4, i, 0, 7); g.fill() }
|
||||
return __cc.call('SetFestivalImageData', '立秋', cv.toDataURL('image/png')).then(f => ({ mode: f.mode, img: (f.image || '').slice(0, 26) })).catch(e => 'ERR:' + e)
|
||||
})()`));
|
||||
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
|
||||
await sleep(600);
|
||||
console.log('photo-cell', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); const bg = getComputedStyle(c, '::before').backgroundImage || ''; const pv = document.querySelector('.fest-card-preview.photo'); const r = pv ? pv.getBoundingClientRect() : null; return { photo: c.classList.contains('has-photo'), jpeg: bg.includes('data:image/jpeg'), previewJpeg: (getComputedStyle(pv).backgroundImage || '').includes('data:image/jpeg'), previewSize: r ? Math.round(r.width) + 'x' + Math.round(r.height) : 'none', activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
|
||||
await shot('tools/shot-fest-photo.png');
|
||||
// 5) 点击"动态插画"卡:格子回退插画但图片仍保留
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.fest-card')][0]?.click(); return 'ok' })()`);
|
||||
await sleep(700);
|
||||
console.log('mode-art', await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo'), kept: !!(s.festivalImages['立秋'] && s.festivalImages['立秋'].image), active: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
|
||||
await shot('tools/shot-fest-mode-art.png');
|
||||
// 6) 点击"自定义图片"卡切回
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.fest-card')][1]?.click(); return 'ok' })()`);
|
||||
await sleep(700);
|
||||
console.log('mode-photo', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { photo: c.classList.contains('has-photo'), active: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
|
||||
// 7) 模拟图片加载失败:festBroken 标记后应立即回退插画
|
||||
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); s.festBroken = { '立秋': true }; return 'set' })()`);
|
||||
await sleep(500);
|
||||
console.log('broken-fallback', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo') } })()`));
|
||||
await evalIn(`(() => { document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').festBroken = {}; return 'clear' })()`);
|
||||
// 8) 移除自定义图,恢复默认插画
|
||||
await evalIn(`__cc.call('RemoveFestivalImage', '立秋')`);
|
||||
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
|
||||
await sleep(500);
|
||||
console.log('restored', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo'), activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim(), upload: !!document.querySelector('.fest-card-upload') } })()`));
|
||||
},
|
||||
async 'square'() {
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(1200);
|
||||
await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); c?.click(); return 'sel' })()`); await sleep(400);
|
||||
await evalIn(`(() => { document.querySelector('.fest-admin-btn')?.click(); return 'opened' })()`); await sleep(400);
|
||||
console.log('square', await evalIn(`(() => { const pv = document.querySelector('.fest-card-preview'); if (!pv) return 'NO_CARD'; const r = pv.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height), square: Math.abs(r.width - r.height) < 1.5 } })()`));
|
||||
await shot('tools/shot-fest-square.png');
|
||||
await evalIn(`(() => { document.querySelector('.fest-admin-modal header button')?.click(); return 'closed' })()`);
|
||||
},
|
||||
async 'todo-style'() {
|
||||
// 一条内容与标题相同(不应重复展示)、一条内容不同(应显示 MD 摘要)
|
||||
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-重复内容', content: '样式验证-重复内容', projectId: 0, dueAt: '', priority: 'medium', status: 'open' })`);
|
||||
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-带摘要', content: '**重点**:这是不同于标题的说明', projectId: 0, dueAt: '', priority: 'high', status: 'open' })`);
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(900);
|
||||
console.log('cards', await evalIn(`(() => { const find = t => [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(t)); const dup = find('样式验证-重复内容'); const md = find('样式验证-带摘要'); return { dupHasClamp: !!dup?.querySelector('.md-clamp'), mdHasClamp: !!md?.querySelector('.md-clamp'), highGlow: md ? getComputedStyle(md).boxShadow.includes('240, 94, 104') : false, medGlow: dup ? getComputedStyle(dup).boxShadow.includes('231, 189, 53') : false, noBar: dup ? getComputedStyle(dup, '::before').content === 'none' : false } })()`));
|
||||
await shot('tools/shot-todo-cards.png');
|
||||
await evalIn(`(async () => { const l = await __cc.call('ListTodos', 'all', 0); for (const x of l.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTodo', x.id); return 'cleaned' })()`);
|
||||
},
|
||||
async 'cal-detail'() {
|
||||
const today = new Date(); const ds = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '详情验证-待办', content: '**加粗**说明\\n\\n- 第一步\\n- 第二步', projectId: 0, dueAt: '${ds}T15:00', priority: 'high', status: 'open' })`);
|
||||
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const pid = s.projects[0]?.id || 0; return __cc.call('SaveTicket', { id: 0, title: '详情验证-工单', description: '> 需求描述引用块', type: 'bug', projectId: pid, startAt: '${ds}', dueAt: '${ds}', priority: 'medium', status: 'open' }) })()`);
|
||||
// 若已在日历页则 hash 不变不会重挂载,先跳工作台再进日历,确保拉到新数据
|
||||
await evalIn(`location.hash = '#/'`); await sleep(400);
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(1000);
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.calendar-cell')].find(x => x.classList.contains('today'))?.click(); return 'sel' })()`); await sleep(500);
|
||||
// 点待办条目 → 详情模态框
|
||||
await evalIn(`(() => { const it = [...document.querySelectorAll('.calendar-item')].find(x => x.querySelector('b')?.textContent === '详情验证-待办'); it?.click(); return !!it })()`); await sleep(500);
|
||||
console.log('todo-detail', await evalIn(`(() => { const m = document.querySelector('.cal-detail-modal'); if (!m) return 'NO_MODAL'; return { title: m.querySelector('h2')?.textContent.trim(), meta: [...m.querySelectorAll('.cal-detail-meta > span')].map(x => x.textContent.trim()), md: !!m.querySelector('.cal-detail-body strong'), tabs: [...m.querySelectorAll('.cal-detail-foot .tabs button')].map(b => b.textContent) } })()`));
|
||||
await shot('tools/shot-cal-detail-todo.png');
|
||||
// 状态流转:待处理 → 进行中
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.cal-detail-foot .tabs button')].find(b => b.textContent === '进行中')?.click(); return 'ok' })()`); await sleep(700);
|
||||
console.log('todo-status', await evalIn(`(() => ({ chip: document.querySelector('.cal-detail-meta .ticket-status')?.textContent, active: document.querySelector('.cal-detail-foot .tabs button.active')?.textContent }))()`));
|
||||
await evalIn(`document.querySelector('.cal-detail-modal header button').click()`); await sleep(400);
|
||||
// 点工单条目 → 详情模态框 → 开始处理
|
||||
await evalIn(`(() => { const it = [...document.querySelectorAll('.calendar-item')].find(x => x.querySelector('b')?.textContent === '详情验证-工单'); it?.click(); return !!it })()`); await sleep(500);
|
||||
console.log('ticket-detail', await evalIn(`(() => { const m = document.querySelector('.cal-detail-modal'); if (!m) return 'NO_MODAL'; return { title: m.querySelector('h2')?.textContent.trim(), quote: !!m.querySelector('.cal-detail-body blockquote'), flows: [...m.querySelectorAll('.cal-detail-flow .flow-btn')].map(b => b.textContent.trim()) } })()`));
|
||||
await shot('tools/shot-cal-detail-ticket.png');
|
||||
await evalIn(`(() => { [...document.querySelectorAll('.cal-detail-flow .flow-btn')][0]?.click(); return 'ok' })()`); await sleep(700);
|
||||
console.log('ticket-status', await evalIn(`(() => ({ chip: document.querySelector('.cal-detail-meta .ticket-status')?.textContent, flows: [...document.querySelectorAll('.cal-detail-flow .flow-btn')].map(b => b.textContent.trim()) }))()`));
|
||||
await evalIn(`document.querySelector('.cal-detail-modal header button').click()`); await sleep(300);
|
||||
// 清理测试数据
|
||||
await evalIn(`(async () => { const ts = await __cc.call('ListTodos', 'all', 0); for (const x of ts.filter(t => t.title.startsWith('详情验证-'))) await __cc.call('DeleteTodo', x.id); const ks = await __cc.call('ListTickets', 'all', 0); for (const x of ks.filter(t => t.title.startsWith('详情验证-'))) await __cc.call('DeleteTicket', x.id); return 'cleaned' })()`);
|
||||
console.log('cleaned', await evalIn(`Promise.all([__cc.call('ListTodos', 'all', 0), __cc.call('ListTickets', 'all', 0)]).then(([a, b]) => a.filter(x => x.title.startsWith('详情验证-')).length + b.filter(x => x.title.startsWith('详情验证-')).length)`));
|
||||
},
|
||||
async 'wb-style'() {
|
||||
const today = new Date(); const ds = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-待办', content: '', projectId: 0, dueAt: '${ds}T18:00', priority: 'high', status: 'open' })`);
|
||||
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const pid = s.projects[0]?.id || 0; return __cc.call('SaveTicket', { id: 0, title: '样式验证-工单', description: '', type: 'bug', projectId: pid, startAt: '${ds}', dueAt: '${ds}T18:00', priority: 'medium', status: 'open' }) })()`);
|
||||
await evalIn(`location.hash = '#/todos'`); await sleep(400);
|
||||
await evalIn(`location.hash = '#/'`); await sleep(900);
|
||||
console.log('wb-items', await evalIn(`(() => { const items = [...document.querySelectorAll('.wb-item')]; const hi = items.find(x => x.classList.contains('high')); if (!hi) return 'NO_HIGH'; const cs = getComputedStyle(hi); return { count: items.length, radius: cs.borderRadius, redBorder: cs.borderColor.includes('240, 94, 104'), glow: cs.boxShadow.includes('240, 94, 104'), noBar: getComputedStyle(hi, '::before').content === 'none' } })()`));
|
||||
await shot('tools/shot-wb-items.png');
|
||||
await evalIn(`location.hash = '#/calendar'`); await sleep(1000);
|
||||
await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell:not(.other-month)')]; const t = cells.find(c => c.classList.contains('today')); t?.click(); return 'clicked' })()`); await sleep(500);
|
||||
console.log('cal-items', await evalIn(`(() => { const items = [...document.querySelectorAll('.calendar-item')]; if (!items.length) return 'NO_ITEMS'; const hi = items.find(x => x.classList.contains('high')) || items[0]; const cs = getComputedStyle(hi); return { count: items.length, radius: cs.borderRadius, glow: cs.boxShadow.includes('240, 94, 104'), hasHigh: items.some(x => x.classList.contains('high')) } })()`));
|
||||
await shot('tools/shot-cal-items.png');
|
||||
await evalIn(`(async () => { const ts = await __cc.call('ListTodos', 'all', 0); for (const x of ts.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTodo', x.id); const ks = await __cc.call('ListTickets', 'all', 0); for (const x of ks.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTicket', x.id); return 'cleaned' })()`);
|
||||
},
|
||||
async cleanup() {
|
||||
// 不依赖视图模式,直接走绑定清理测试待办
|
||||
console.log('left', await evalIn(`(async () => { const l = await __cc.call('ListTodos', 'all', 0); for (const x of l.filter(t => t.title.includes(${JSON.stringify(TODO_TITLE)}))) await __cc.call('DeleteTodo', x.id); const after = await __cc.call('ListTodos', 'all', 0); return after.filter(t => t.title.includes(${JSON.stringify(TODO_TITLE)})).length })()`));
|
||||
},
|
||||
};
|
||||
|
||||
(async () => {
|
||||
await connect();
|
||||
const list = process.argv.slice(2);
|
||||
if (!list.length || list.some(s => !stages[s])) throw new Error('unknown stage in: ' + list.join(' '));
|
||||
for (const stage of list) { console.log('== stage:', stage); await stages[stage](); }
|
||||
await sleep(200); ws.close();
|
||||
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
|
||||
BIN
tools/verify-rail-2x2.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
tools/verify-rail-avatar-out.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
tools/verify-rail-popover.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
tools/verify-rail-popover2.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
tools/verify-rail.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
tools/verify-scope-done.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
tools/verify-scope-loading.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
tools/verify-scope.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
tools/verify-scope2.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
tools/verify-settings.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
tools/verify-team-badge-num.png
Normal file
|
After Width: | Height: | Size: 348 KiB |
BIN
tools/verify-team-badges.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
tools/verify-team-badges2.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
tools/verify-team-badges3.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
tools/verify-team-home-empty.png
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
tools/verify-team-home.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
tools/verify-team-msgs.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
tools/verify-team-profile-info.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
tools/verify-team-profile-top.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
tools/verify-team-profile.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
tools/verify-team-profile2.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
tools/verify-team-reports.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
tools/verify-team-share-select.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
tools/verify-team-switcher.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
tools/verify-team-task-modal.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
tools/verify-team-tasks-empty.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
tools/verify-team-tasks.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
tools/verify-team-todos-clean.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
tools/verify-team-wb.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
tools/verify-team-wbgrid.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
tools/verify-today.png
Normal file
|
After Width: | Height: | Size: 399 KiB |
BIN
tools/verify-wb.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
tools/verify-window.png
Normal file
|
After Width: | Height: | Size: 312 KiB |