@hasna/testers 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -2
- package/dist/cli/index.js +1845 -429
- package/dist/db/auth-presets.d.ts +20 -0
- package/dist/db/auth-presets.d.ts.map +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/screenshots.d.ts +3 -0
- package/dist/db/screenshots.d.ts.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1815 -358
- package/dist/lib/ai-client.d.ts +6 -0
- package/dist/lib/ai-client.d.ts.map +1 -1
- package/dist/lib/costs.d.ts +36 -0
- package/dist/lib/costs.d.ts.map +1 -0
- package/dist/lib/diff.d.ts +25 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/init.d.ts +28 -0
- package/dist/lib/init.d.ts.map +1 -0
- package/dist/lib/report.d.ts +4 -0
- package/dist/lib/report.d.ts.map +1 -0
- package/dist/lib/runner.d.ts +12 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/screenshotter.d.ts +27 -25
- package/dist/lib/screenshotter.d.ts.map +1 -1
- package/dist/lib/smoke.d.ts +25 -0
- package/dist/lib/smoke.d.ts.map +1 -0
- package/dist/lib/templates.d.ts +5 -0
- package/dist/lib/templates.d.ts.map +1 -0
- package/dist/lib/watch.d.ts +9 -0
- package/dist/lib/watch.d.ts.map +1 -0
- package/dist/lib/webhooks.d.ts +41 -0
- package/dist/lib/webhooks.d.ts.map +1 -0
- package/dist/mcp/index.js +221 -31
- package/dist/server/index.js +138 -18
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -4080,7 +4080,10 @@ function screenshotFromRow(row) {
|
|
|
4080
4080
|
filePath: row.file_path,
|
|
4081
4081
|
width: row.width,
|
|
4082
4082
|
height: row.height,
|
|
4083
|
-
timestamp: row.timestamp
|
|
4083
|
+
timestamp: row.timestamp,
|
|
4084
|
+
description: row.description,
|
|
4085
|
+
pageUrl: row.page_url,
|
|
4086
|
+
thumbnailPath: row.thumbnail_path
|
|
4084
4087
|
};
|
|
4085
4088
|
}
|
|
4086
4089
|
function scheduleFromRow(row) {
|
|
@@ -4289,6 +4292,34 @@ var MIGRATIONS = [
|
|
|
4289
4292
|
CREATE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id);
|
|
4290
4293
|
CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
|
|
4291
4294
|
CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run_at);
|
|
4295
|
+
`,
|
|
4296
|
+
`
|
|
4297
|
+
ALTER TABLE screenshots ADD COLUMN description TEXT;
|
|
4298
|
+
ALTER TABLE screenshots ADD COLUMN page_url TEXT;
|
|
4299
|
+
ALTER TABLE screenshots ADD COLUMN thumbnail_path TEXT;
|
|
4300
|
+
`,
|
|
4301
|
+
`
|
|
4302
|
+
CREATE TABLE IF NOT EXISTS auth_presets (
|
|
4303
|
+
id TEXT PRIMARY KEY,
|
|
4304
|
+
name TEXT NOT NULL UNIQUE,
|
|
4305
|
+
email TEXT NOT NULL,
|
|
4306
|
+
password TEXT NOT NULL,
|
|
4307
|
+
login_path TEXT DEFAULT '/login',
|
|
4308
|
+
metadata TEXT DEFAULT '{}',
|
|
4309
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4310
|
+
);
|
|
4311
|
+
`,
|
|
4312
|
+
`
|
|
4313
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
4314
|
+
id TEXT PRIMARY KEY,
|
|
4315
|
+
url TEXT NOT NULL,
|
|
4316
|
+
events TEXT NOT NULL DEFAULT '["failed"]',
|
|
4317
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
4318
|
+
secret TEXT,
|
|
4319
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
4320
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4321
|
+
);
|
|
4322
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active);
|
|
4292
4323
|
`
|
|
4293
4324
|
];
|
|
4294
4325
|
function applyMigrations(database) {
|
|
@@ -4686,9 +4717,9 @@ function createScreenshot(input) {
|
|
|
4686
4717
|
const id = uuid();
|
|
4687
4718
|
const timestamp = now();
|
|
4688
4719
|
db2.query(`
|
|
4689
|
-
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp)
|
|
4690
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4691
|
-
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp);
|
|
4720
|
+
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
|
|
4721
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4722
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
|
|
4692
4723
|
return getScreenshot(id);
|
|
4693
4724
|
}
|
|
4694
4725
|
function getScreenshot(id) {
|
|
@@ -4805,7 +4836,7 @@ async function closeBrowser(browser) {
|
|
|
4805
4836
|
}
|
|
4806
4837
|
|
|
4807
4838
|
// src/lib/screenshotter.ts
|
|
4808
|
-
import { mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
4839
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync } from "fs";
|
|
4809
4840
|
import { join as join2 } from "path";
|
|
4810
4841
|
import { homedir as homedir2 } from "os";
|
|
4811
4842
|
function slugify(text) {
|
|
@@ -4814,16 +4845,51 @@ function slugify(text) {
|
|
|
4814
4845
|
function generateFilename(stepNumber, action) {
|
|
4815
4846
|
const padded = String(stepNumber).padStart(3, "0");
|
|
4816
4847
|
const slug = slugify(action);
|
|
4817
|
-
return `${padded}
|
|
4848
|
+
return `${padded}_${slug}.png`;
|
|
4849
|
+
}
|
|
4850
|
+
function formatDate(date) {
|
|
4851
|
+
return date.toISOString().slice(0, 10);
|
|
4852
|
+
}
|
|
4853
|
+
function formatTime(date) {
|
|
4854
|
+
return date.toISOString().slice(11, 19).replace(/:/g, "-");
|
|
4818
4855
|
}
|
|
4819
|
-
function getScreenshotDir(baseDir, runId, scenarioSlug) {
|
|
4820
|
-
|
|
4856
|
+
function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp) {
|
|
4857
|
+
const now2 = timestamp ?? new Date;
|
|
4858
|
+
const project = projectName ?? "default";
|
|
4859
|
+
const dateDir = formatDate(now2);
|
|
4860
|
+
const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
|
|
4861
|
+
return join2(baseDir, project, dateDir, timeDir, scenarioSlug);
|
|
4821
4862
|
}
|
|
4822
4863
|
function ensureDir(dirPath) {
|
|
4823
4864
|
if (!existsSync2(dirPath)) {
|
|
4824
4865
|
mkdirSync2(dirPath, { recursive: true });
|
|
4825
4866
|
}
|
|
4826
4867
|
}
|
|
4868
|
+
function writeMetaSidecar(screenshotPath, meta) {
|
|
4869
|
+
const metaPath = screenshotPath.replace(/\.png$/, ".meta.json").replace(/\.jpeg$/, ".meta.json");
|
|
4870
|
+
try {
|
|
4871
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
4872
|
+
} catch {}
|
|
4873
|
+
}
|
|
4874
|
+
async function generateThumbnail(page, screenshotDir, filename) {
|
|
4875
|
+
try {
|
|
4876
|
+
const thumbDir = join2(screenshotDir, "_thumbnail");
|
|
4877
|
+
ensureDir(thumbDir);
|
|
4878
|
+
const thumbFilename = filename.replace(/\.(png|jpeg)$/, ".thumb.$1");
|
|
4879
|
+
const thumbPath = join2(thumbDir, thumbFilename);
|
|
4880
|
+
const viewport = page.viewportSize();
|
|
4881
|
+
if (viewport) {
|
|
4882
|
+
await page.screenshot({
|
|
4883
|
+
path: thumbPath,
|
|
4884
|
+
type: "png",
|
|
4885
|
+
clip: { x: 0, y: 0, width: Math.min(viewport.width, 1280), height: Math.min(viewport.height, 720) }
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4888
|
+
return thumbPath;
|
|
4889
|
+
} catch {
|
|
4890
|
+
return null;
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4827
4893
|
var DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
|
|
4828
4894
|
|
|
4829
4895
|
class Screenshotter {
|
|
@@ -4831,15 +4897,20 @@ class Screenshotter {
|
|
|
4831
4897
|
format;
|
|
4832
4898
|
quality;
|
|
4833
4899
|
fullPage;
|
|
4900
|
+
projectName;
|
|
4901
|
+
runTimestamp;
|
|
4834
4902
|
constructor(options = {}) {
|
|
4835
4903
|
this.baseDir = options.baseDir ?? DEFAULT_BASE_DIR;
|
|
4836
4904
|
this.format = options.format ?? "png";
|
|
4837
4905
|
this.quality = options.quality ?? 90;
|
|
4838
4906
|
this.fullPage = options.fullPage ?? false;
|
|
4907
|
+
this.projectName = options.projectName ?? "default";
|
|
4908
|
+
this.runTimestamp = new Date;
|
|
4839
4909
|
}
|
|
4840
4910
|
async capture(page, options) {
|
|
4841
|
-
const
|
|
4842
|
-
const
|
|
4911
|
+
const action = options.description ?? options.action;
|
|
4912
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
4913
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
4843
4914
|
const filePath = join2(dir, filename);
|
|
4844
4915
|
ensureDir(dir);
|
|
4845
4916
|
await page.screenshot({
|
|
@@ -4849,16 +4920,32 @@ class Screenshotter {
|
|
|
4849
4920
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
4850
4921
|
});
|
|
4851
4922
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
4923
|
+
const pageUrl = page.url();
|
|
4924
|
+
const timestamp = new Date().toISOString();
|
|
4925
|
+
writeMetaSidecar(filePath, {
|
|
4926
|
+
stepNumber: options.stepNumber,
|
|
4927
|
+
action: options.action,
|
|
4928
|
+
description: options.description ?? null,
|
|
4929
|
+
pageUrl,
|
|
4930
|
+
viewport,
|
|
4931
|
+
timestamp,
|
|
4932
|
+
filePath
|
|
4933
|
+
});
|
|
4934
|
+
const thumbnailPath = await generateThumbnail(page, dir, filename);
|
|
4852
4935
|
return {
|
|
4853
4936
|
filePath,
|
|
4854
4937
|
width: viewport.width,
|
|
4855
4938
|
height: viewport.height,
|
|
4856
|
-
timestamp
|
|
4939
|
+
timestamp,
|
|
4940
|
+
description: options.description ?? null,
|
|
4941
|
+
pageUrl,
|
|
4942
|
+
thumbnailPath
|
|
4857
4943
|
};
|
|
4858
4944
|
}
|
|
4859
4945
|
async captureFullPage(page, options) {
|
|
4860
|
-
const
|
|
4861
|
-
const
|
|
4946
|
+
const action = options.description ?? options.action;
|
|
4947
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
4948
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
4862
4949
|
const filePath = join2(dir, filename);
|
|
4863
4950
|
ensureDir(dir);
|
|
4864
4951
|
await page.screenshot({
|
|
@@ -4868,16 +4955,32 @@ class Screenshotter {
|
|
|
4868
4955
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
4869
4956
|
});
|
|
4870
4957
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
4958
|
+
const pageUrl = page.url();
|
|
4959
|
+
const timestamp = new Date().toISOString();
|
|
4960
|
+
writeMetaSidecar(filePath, {
|
|
4961
|
+
stepNumber: options.stepNumber,
|
|
4962
|
+
action: options.action,
|
|
4963
|
+
description: options.description ?? null,
|
|
4964
|
+
pageUrl,
|
|
4965
|
+
viewport,
|
|
4966
|
+
timestamp,
|
|
4967
|
+
filePath
|
|
4968
|
+
});
|
|
4969
|
+
const thumbnailPath = await generateThumbnail(page, dir, filename);
|
|
4871
4970
|
return {
|
|
4872
4971
|
filePath,
|
|
4873
4972
|
width: viewport.width,
|
|
4874
4973
|
height: viewport.height,
|
|
4875
|
-
timestamp
|
|
4974
|
+
timestamp,
|
|
4975
|
+
description: options.description ?? null,
|
|
4976
|
+
pageUrl,
|
|
4977
|
+
thumbnailPath
|
|
4876
4978
|
};
|
|
4877
4979
|
}
|
|
4878
4980
|
async captureElement(page, selector, options) {
|
|
4879
|
-
const
|
|
4880
|
-
const
|
|
4981
|
+
const action = options.description ?? options.action;
|
|
4982
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
4983
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
4881
4984
|
const filePath = join2(dir, filename);
|
|
4882
4985
|
ensureDir(dir);
|
|
4883
4986
|
await page.locator(selector).screenshot({
|
|
@@ -4886,11 +4989,25 @@ class Screenshotter {
|
|
|
4886
4989
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
4887
4990
|
});
|
|
4888
4991
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
4992
|
+
const pageUrl = page.url();
|
|
4993
|
+
const timestamp = new Date().toISOString();
|
|
4994
|
+
writeMetaSidecar(filePath, {
|
|
4995
|
+
stepNumber: options.stepNumber,
|
|
4996
|
+
action: options.action,
|
|
4997
|
+
description: options.description ?? null,
|
|
4998
|
+
pageUrl,
|
|
4999
|
+
viewport,
|
|
5000
|
+
timestamp,
|
|
5001
|
+
filePath
|
|
5002
|
+
});
|
|
4889
5003
|
return {
|
|
4890
5004
|
filePath,
|
|
4891
5005
|
width: viewport.width,
|
|
4892
5006
|
height: viewport.height,
|
|
4893
|
-
timestamp
|
|
5007
|
+
timestamp,
|
|
5008
|
+
description: options.description ?? null,
|
|
5009
|
+
pageUrl,
|
|
5010
|
+
thumbnailPath: null
|
|
4894
5011
|
};
|
|
4895
5012
|
}
|
|
4896
5013
|
}
|
|
@@ -5673,7 +5790,10 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
5673
5790
|
action: ss.action,
|
|
5674
5791
|
filePath: ss.filePath,
|
|
5675
5792
|
width: ss.width,
|
|
5676
|
-
height: ss.height
|
|
5793
|
+
height: ss.height,
|
|
5794
|
+
description: ss.description,
|
|
5795
|
+
pageUrl: ss.pageUrl,
|
|
5796
|
+
thumbnailPath: ss.thumbnailPath
|
|
5677
5797
|
});
|
|
5678
5798
|
emit({ type: "screenshot:captured", screenshotPath: ss.filePath, scenarioId: scenario.id, runId });
|
|
5679
5799
|
}
|
|
@@ -5771,6 +5891,79 @@ async function runByFilter(options) {
|
|
|
5771
5891
|
}
|
|
5772
5892
|
return runBatch(scenarios, options);
|
|
5773
5893
|
}
|
|
5894
|
+
function startRunAsync(options) {
|
|
5895
|
+
const config = loadConfig();
|
|
5896
|
+
const model = resolveModel(options.model ?? config.defaultModel);
|
|
5897
|
+
let scenarios;
|
|
5898
|
+
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
5899
|
+
const all = listScenarios({ projectId: options.projectId });
|
|
5900
|
+
scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
|
|
5901
|
+
} else {
|
|
5902
|
+
scenarios = listScenarios({
|
|
5903
|
+
projectId: options.projectId,
|
|
5904
|
+
tags: options.tags,
|
|
5905
|
+
priority: options.priority
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
const parallel = options.parallel ?? 1;
|
|
5909
|
+
const run = createRun({
|
|
5910
|
+
url: options.url,
|
|
5911
|
+
model,
|
|
5912
|
+
headed: options.headed,
|
|
5913
|
+
parallel,
|
|
5914
|
+
projectId: options.projectId
|
|
5915
|
+
});
|
|
5916
|
+
if (scenarios.length === 0) {
|
|
5917
|
+
updateRun(run.id, { status: "passed", total: 0, finished_at: new Date().toISOString() });
|
|
5918
|
+
return { runId: run.id, scenarioCount: 0 };
|
|
5919
|
+
}
|
|
5920
|
+
updateRun(run.id, { status: "running", total: scenarios.length });
|
|
5921
|
+
(async () => {
|
|
5922
|
+
const results = [];
|
|
5923
|
+
try {
|
|
5924
|
+
if (parallel <= 1) {
|
|
5925
|
+
for (const scenario of scenarios) {
|
|
5926
|
+
const result = await runSingleScenario(scenario, run.id, options);
|
|
5927
|
+
results.push(result);
|
|
5928
|
+
}
|
|
5929
|
+
} else {
|
|
5930
|
+
const queue = [...scenarios];
|
|
5931
|
+
const running = [];
|
|
5932
|
+
const processNext = async () => {
|
|
5933
|
+
const scenario = queue.shift();
|
|
5934
|
+
if (!scenario)
|
|
5935
|
+
return;
|
|
5936
|
+
const result = await runSingleScenario(scenario, run.id, options);
|
|
5937
|
+
results.push(result);
|
|
5938
|
+
await processNext();
|
|
5939
|
+
};
|
|
5940
|
+
const workers = Math.min(parallel, scenarios.length);
|
|
5941
|
+
for (let i = 0;i < workers; i++) {
|
|
5942
|
+
running.push(processNext());
|
|
5943
|
+
}
|
|
5944
|
+
await Promise.all(running);
|
|
5945
|
+
}
|
|
5946
|
+
const passed = results.filter((r) => r.status === "passed").length;
|
|
5947
|
+
const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
|
|
5948
|
+
updateRun(run.id, {
|
|
5949
|
+
status: failed > 0 ? "failed" : "passed",
|
|
5950
|
+
passed,
|
|
5951
|
+
failed,
|
|
5952
|
+
total: scenarios.length,
|
|
5953
|
+
finished_at: new Date().toISOString()
|
|
5954
|
+
});
|
|
5955
|
+
emit({ type: "run:complete", runId: run.id });
|
|
5956
|
+
} catch (error) {
|
|
5957
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5958
|
+
updateRun(run.id, {
|
|
5959
|
+
status: "failed",
|
|
5960
|
+
finished_at: new Date().toISOString()
|
|
5961
|
+
});
|
|
5962
|
+
emit({ type: "run:complete", runId: run.id, error: errorMsg });
|
|
5963
|
+
}
|
|
5964
|
+
})();
|
|
5965
|
+
return { runId: run.id, scenarioCount: scenarios.length };
|
|
5966
|
+
}
|
|
5774
5967
|
function estimateCost(model, tokens) {
|
|
5775
5968
|
const costs = {
|
|
5776
5969
|
"claude-haiku-4-5-20251001": 0.1,
|
|
@@ -6223,7 +6416,7 @@ class Scheduler {
|
|
|
6223
6416
|
|
|
6224
6417
|
// src/mcp/index.ts
|
|
6225
6418
|
var server = new McpServer({
|
|
6226
|
-
name: "testers
|
|
6419
|
+
name: "testers",
|
|
6227
6420
|
version: "0.0.1"
|
|
6228
6421
|
});
|
|
6229
6422
|
server.tool("create_scenario", "Create a new test scenario", {
|
|
@@ -6337,18 +6530,15 @@ server.tool("run_scenarios", "Run test scenarios against a URL", {
|
|
|
6337
6530
|
parallel: exports_external.number().optional().describe("Number of parallel workers")
|
|
6338
6531
|
}, async ({ url, tags, scenarioIds, priority, model, headed, parallel }) => {
|
|
6339
6532
|
try {
|
|
6340
|
-
const {
|
|
6341
|
-
const passed = results.filter((r) => r.status === "passed").length;
|
|
6342
|
-
const failed = results.filter((r) => r.status === "failed" || r.status === "error").length;
|
|
6343
|
-
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
6533
|
+
const { runId, scenarioCount } = startRunAsync({ url, tags, scenarioIds, priority, model, headed, parallel });
|
|
6344
6534
|
const text = [
|
|
6345
|
-
`Run
|
|
6346
|
-
`
|
|
6347
|
-
`
|
|
6348
|
-
`
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
].
|
|
6535
|
+
`Run started: ${runId}`,
|
|
6536
|
+
`Scenarios: ${scenarioCount}`,
|
|
6537
|
+
`URL: ${url}`,
|
|
6538
|
+
`Status: running (async)`,
|
|
6539
|
+
``,
|
|
6540
|
+
`Poll with get_run to check progress.`
|
|
6541
|
+
].join(`
|
|
6352
6542
|
`);
|
|
6353
6543
|
return { content: [{ type: "text", text }] };
|
|
6354
6544
|
} catch (error) {
|
|
@@ -6592,6 +6782,6 @@ async function main() {
|
|
|
6592
6782
|
await server.connect(transport);
|
|
6593
6783
|
}
|
|
6594
6784
|
main().catch((error) => {
|
|
6595
|
-
console.error("Failed to start testers
|
|
6785
|
+
console.error("Failed to start testers:", error);
|
|
6596
6786
|
process.exit(1);
|
|
6597
6787
|
});
|
package/dist/server/index.js
CHANGED
|
@@ -77,7 +77,10 @@ function screenshotFromRow(row) {
|
|
|
77
77
|
filePath: row.file_path,
|
|
78
78
|
width: row.width,
|
|
79
79
|
height: row.height,
|
|
80
|
-
timestamp: row.timestamp
|
|
80
|
+
timestamp: row.timestamp,
|
|
81
|
+
description: row.description,
|
|
82
|
+
pageUrl: row.page_url,
|
|
83
|
+
thumbnailPath: row.thumbnail_path
|
|
81
84
|
};
|
|
82
85
|
}
|
|
83
86
|
function scheduleFromRow(row) {
|
|
@@ -279,6 +282,34 @@ var MIGRATIONS = [
|
|
|
279
282
|
CREATE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id);
|
|
280
283
|
CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
|
|
281
284
|
CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run_at);
|
|
285
|
+
`,
|
|
286
|
+
`
|
|
287
|
+
ALTER TABLE screenshots ADD COLUMN description TEXT;
|
|
288
|
+
ALTER TABLE screenshots ADD COLUMN page_url TEXT;
|
|
289
|
+
ALTER TABLE screenshots ADD COLUMN thumbnail_path TEXT;
|
|
290
|
+
`,
|
|
291
|
+
`
|
|
292
|
+
CREATE TABLE IF NOT EXISTS auth_presets (
|
|
293
|
+
id TEXT PRIMARY KEY,
|
|
294
|
+
name TEXT NOT NULL UNIQUE,
|
|
295
|
+
email TEXT NOT NULL,
|
|
296
|
+
password TEXT NOT NULL,
|
|
297
|
+
login_path TEXT DEFAULT '/login',
|
|
298
|
+
metadata TEXT DEFAULT '{}',
|
|
299
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
300
|
+
);
|
|
301
|
+
`,
|
|
302
|
+
`
|
|
303
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
304
|
+
id TEXT PRIMARY KEY,
|
|
305
|
+
url TEXT NOT NULL,
|
|
306
|
+
events TEXT NOT NULL DEFAULT '["failed"]',
|
|
307
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
308
|
+
secret TEXT,
|
|
309
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
310
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active);
|
|
282
313
|
`
|
|
283
314
|
];
|
|
284
315
|
function applyMigrations(database) {
|
|
@@ -679,9 +710,9 @@ function createScreenshot(input) {
|
|
|
679
710
|
const id = uuid();
|
|
680
711
|
const timestamp = now();
|
|
681
712
|
db2.query(`
|
|
682
|
-
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp)
|
|
683
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
684
|
-
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp);
|
|
713
|
+
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
|
|
714
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
715
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
|
|
685
716
|
return getScreenshot(id);
|
|
686
717
|
}
|
|
687
718
|
function getScreenshot(id) {
|
|
@@ -739,7 +770,7 @@ async function closeBrowser(browser) {
|
|
|
739
770
|
}
|
|
740
771
|
|
|
741
772
|
// src/lib/screenshotter.ts
|
|
742
|
-
import { mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
773
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync } from "fs";
|
|
743
774
|
import { join as join2 } from "path";
|
|
744
775
|
import { homedir as homedir2 } from "os";
|
|
745
776
|
function slugify(text) {
|
|
@@ -748,16 +779,51 @@ function slugify(text) {
|
|
|
748
779
|
function generateFilename(stepNumber, action) {
|
|
749
780
|
const padded = String(stepNumber).padStart(3, "0");
|
|
750
781
|
const slug = slugify(action);
|
|
751
|
-
return `${padded}
|
|
782
|
+
return `${padded}_${slug}.png`;
|
|
783
|
+
}
|
|
784
|
+
function formatDate(date) {
|
|
785
|
+
return date.toISOString().slice(0, 10);
|
|
752
786
|
}
|
|
753
|
-
function
|
|
754
|
-
return
|
|
787
|
+
function formatTime(date) {
|
|
788
|
+
return date.toISOString().slice(11, 19).replace(/:/g, "-");
|
|
789
|
+
}
|
|
790
|
+
function getScreenshotDir(baseDir, runId, scenarioSlug, projectName, timestamp) {
|
|
791
|
+
const now2 = timestamp ?? new Date;
|
|
792
|
+
const project = projectName ?? "default";
|
|
793
|
+
const dateDir = formatDate(now2);
|
|
794
|
+
const timeDir = `${formatTime(now2)}_${runId.slice(0, 8)}`;
|
|
795
|
+
return join2(baseDir, project, dateDir, timeDir, scenarioSlug);
|
|
755
796
|
}
|
|
756
797
|
function ensureDir(dirPath) {
|
|
757
798
|
if (!existsSync2(dirPath)) {
|
|
758
799
|
mkdirSync2(dirPath, { recursive: true });
|
|
759
800
|
}
|
|
760
801
|
}
|
|
802
|
+
function writeMetaSidecar(screenshotPath, meta) {
|
|
803
|
+
const metaPath = screenshotPath.replace(/\.png$/, ".meta.json").replace(/\.jpeg$/, ".meta.json");
|
|
804
|
+
try {
|
|
805
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
806
|
+
} catch {}
|
|
807
|
+
}
|
|
808
|
+
async function generateThumbnail(page, screenshotDir, filename) {
|
|
809
|
+
try {
|
|
810
|
+
const thumbDir = join2(screenshotDir, "_thumbnail");
|
|
811
|
+
ensureDir(thumbDir);
|
|
812
|
+
const thumbFilename = filename.replace(/\.(png|jpeg)$/, ".thumb.$1");
|
|
813
|
+
const thumbPath = join2(thumbDir, thumbFilename);
|
|
814
|
+
const viewport = page.viewportSize();
|
|
815
|
+
if (viewport) {
|
|
816
|
+
await page.screenshot({
|
|
817
|
+
path: thumbPath,
|
|
818
|
+
type: "png",
|
|
819
|
+
clip: { x: 0, y: 0, width: Math.min(viewport.width, 1280), height: Math.min(viewport.height, 720) }
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return thumbPath;
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
761
827
|
var DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
|
|
762
828
|
|
|
763
829
|
class Screenshotter {
|
|
@@ -765,15 +831,20 @@ class Screenshotter {
|
|
|
765
831
|
format;
|
|
766
832
|
quality;
|
|
767
833
|
fullPage;
|
|
834
|
+
projectName;
|
|
835
|
+
runTimestamp;
|
|
768
836
|
constructor(options = {}) {
|
|
769
837
|
this.baseDir = options.baseDir ?? DEFAULT_BASE_DIR;
|
|
770
838
|
this.format = options.format ?? "png";
|
|
771
839
|
this.quality = options.quality ?? 90;
|
|
772
840
|
this.fullPage = options.fullPage ?? false;
|
|
841
|
+
this.projectName = options.projectName ?? "default";
|
|
842
|
+
this.runTimestamp = new Date;
|
|
773
843
|
}
|
|
774
844
|
async capture(page, options) {
|
|
775
|
-
const
|
|
776
|
-
const
|
|
845
|
+
const action = options.description ?? options.action;
|
|
846
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
847
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
777
848
|
const filePath = join2(dir, filename);
|
|
778
849
|
ensureDir(dir);
|
|
779
850
|
await page.screenshot({
|
|
@@ -783,16 +854,32 @@ class Screenshotter {
|
|
|
783
854
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
784
855
|
});
|
|
785
856
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
857
|
+
const pageUrl = page.url();
|
|
858
|
+
const timestamp = new Date().toISOString();
|
|
859
|
+
writeMetaSidecar(filePath, {
|
|
860
|
+
stepNumber: options.stepNumber,
|
|
861
|
+
action: options.action,
|
|
862
|
+
description: options.description ?? null,
|
|
863
|
+
pageUrl,
|
|
864
|
+
viewport,
|
|
865
|
+
timestamp,
|
|
866
|
+
filePath
|
|
867
|
+
});
|
|
868
|
+
const thumbnailPath = await generateThumbnail(page, dir, filename);
|
|
786
869
|
return {
|
|
787
870
|
filePath,
|
|
788
871
|
width: viewport.width,
|
|
789
872
|
height: viewport.height,
|
|
790
|
-
timestamp
|
|
873
|
+
timestamp,
|
|
874
|
+
description: options.description ?? null,
|
|
875
|
+
pageUrl,
|
|
876
|
+
thumbnailPath
|
|
791
877
|
};
|
|
792
878
|
}
|
|
793
879
|
async captureFullPage(page, options) {
|
|
794
|
-
const
|
|
795
|
-
const
|
|
880
|
+
const action = options.description ?? options.action;
|
|
881
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
882
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
796
883
|
const filePath = join2(dir, filename);
|
|
797
884
|
ensureDir(dir);
|
|
798
885
|
await page.screenshot({
|
|
@@ -802,16 +889,32 @@ class Screenshotter {
|
|
|
802
889
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
803
890
|
});
|
|
804
891
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
892
|
+
const pageUrl = page.url();
|
|
893
|
+
const timestamp = new Date().toISOString();
|
|
894
|
+
writeMetaSidecar(filePath, {
|
|
895
|
+
stepNumber: options.stepNumber,
|
|
896
|
+
action: options.action,
|
|
897
|
+
description: options.description ?? null,
|
|
898
|
+
pageUrl,
|
|
899
|
+
viewport,
|
|
900
|
+
timestamp,
|
|
901
|
+
filePath
|
|
902
|
+
});
|
|
903
|
+
const thumbnailPath = await generateThumbnail(page, dir, filename);
|
|
805
904
|
return {
|
|
806
905
|
filePath,
|
|
807
906
|
width: viewport.width,
|
|
808
907
|
height: viewport.height,
|
|
809
|
-
timestamp
|
|
908
|
+
timestamp,
|
|
909
|
+
description: options.description ?? null,
|
|
910
|
+
pageUrl,
|
|
911
|
+
thumbnailPath
|
|
810
912
|
};
|
|
811
913
|
}
|
|
812
914
|
async captureElement(page, selector, options) {
|
|
813
|
-
const
|
|
814
|
-
const
|
|
915
|
+
const action = options.description ?? options.action;
|
|
916
|
+
const dir = getScreenshotDir(this.baseDir, options.runId, options.scenarioSlug, this.projectName, this.runTimestamp);
|
|
917
|
+
const filename = generateFilename(options.stepNumber, action);
|
|
815
918
|
const filePath = join2(dir, filename);
|
|
816
919
|
ensureDir(dir);
|
|
817
920
|
await page.locator(selector).screenshot({
|
|
@@ -820,11 +923,25 @@ class Screenshotter {
|
|
|
820
923
|
quality: this.format === "jpeg" ? this.quality : undefined
|
|
821
924
|
});
|
|
822
925
|
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
926
|
+
const pageUrl = page.url();
|
|
927
|
+
const timestamp = new Date().toISOString();
|
|
928
|
+
writeMetaSidecar(filePath, {
|
|
929
|
+
stepNumber: options.stepNumber,
|
|
930
|
+
action: options.action,
|
|
931
|
+
description: options.description ?? null,
|
|
932
|
+
pageUrl,
|
|
933
|
+
viewport,
|
|
934
|
+
timestamp,
|
|
935
|
+
filePath
|
|
936
|
+
});
|
|
823
937
|
return {
|
|
824
938
|
filePath,
|
|
825
939
|
width: viewport.width,
|
|
826
940
|
height: viewport.height,
|
|
827
|
-
timestamp
|
|
941
|
+
timestamp,
|
|
942
|
+
description: options.description ?? null,
|
|
943
|
+
pageUrl,
|
|
944
|
+
thumbnailPath: null
|
|
828
945
|
};
|
|
829
946
|
}
|
|
830
947
|
}
|
|
@@ -1607,7 +1724,10 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
1607
1724
|
action: ss.action,
|
|
1608
1725
|
filePath: ss.filePath,
|
|
1609
1726
|
width: ss.width,
|
|
1610
|
-
height: ss.height
|
|
1727
|
+
height: ss.height,
|
|
1728
|
+
description: ss.description,
|
|
1729
|
+
pageUrl: ss.pageUrl,
|
|
1730
|
+
thumbnailPath: ss.thumbnailPath
|
|
1611
1731
|
});
|
|
1612
1732
|
emit({ type: "screenshot:captured", screenshotPath: ss.filePath, scenarioId: scenario.id, runId });
|
|
1613
1733
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -79,6 +79,9 @@ export interface ScreenshotRow {
|
|
|
79
79
|
width: number;
|
|
80
80
|
height: number;
|
|
81
81
|
timestamp: string;
|
|
82
|
+
description: string | null;
|
|
83
|
+
page_url: string | null;
|
|
84
|
+
thumbnail_path: string | null;
|
|
82
85
|
}
|
|
83
86
|
export interface ScheduleRow {
|
|
84
87
|
id: string;
|
|
@@ -174,6 +177,9 @@ export interface Screenshot {
|
|
|
174
177
|
width: number;
|
|
175
178
|
height: number;
|
|
176
179
|
timestamp: string;
|
|
180
|
+
description: string | null;
|
|
181
|
+
pageUrl: string | null;
|
|
182
|
+
thumbnailPath: string | null;
|
|
177
183
|
}
|
|
178
184
|
export interface Schedule {
|
|
179
185
|
id: string;
|