mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
improved playright integration
This commit is contained in:
1 parent
0cf1dc0f55
commit
f099e7be3a
17 files changed
+1208
-7
No files matched your search
@@ -458,6 +458,7 @@ Use these for stable element targeting:
|
||||
| `tests/e2e/agent-testability.spec.ts` | Infrastructure validation tests |
|
||||
| `scripts/playwright/run-extension-persistent.mjs` | Dedicated persistent-context launcher for extension smoke tests |
|
||||
| `docs/testing/playwright-integration-recommendations-2026-02-22.md` | Playwright/extension integration recommendations and follow-ups |
|
||||
| `docs/testing/playwright-function-coverage.md` | Playwright coverage integration, latest baseline metrics, and improvement plan |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Playwright Function Coverage + Console Diagnostics
|
||||
|
||||
Use this workflow to measure how much of bitvid's runtime code is exercised by E2E Playwright tests and to export browser console/page errors for triage.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
npm run test:e2e:coverage
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Builds `dist/`
|
||||
2. Runs `tests/e2e` on Chromium with browser JS coverage enabled
|
||||
3. Captures browser `console` and `pageerror` output for every test
|
||||
4. Generates aggregate reports
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `artifacts/playwright-coverage/raw/*.json`
|
||||
- Per-test raw coverage + console/page-error logs
|
||||
- `artifacts/playwright-coverage/function-coverage-summary.json`
|
||||
- Machine-readable function coverage report
|
||||
- `artifacts/playwright-coverage/function-coverage-summary.md`
|
||||
- Human-readable summary with coverage percentage and low-coverage files
|
||||
- `artifacts/playwright-coverage/console-log-summary.json`
|
||||
- Full exported console/page-error logs
|
||||
- `artifacts/playwright-coverage/console-issues.log`
|
||||
- Grouped issue signals for fast triage
|
||||
|
||||
## Notes
|
||||
|
||||
- Coverage is captured from Chromium runtime execution (`page.coverage`), then combined with static function estimates for source files in `js/` and `torrent/`.
|
||||
- Firefox coverage is not included in this report because Playwright JS coverage is Chromium-only.
|
||||
|
||||
## Current Baseline (2026-02-22)
|
||||
|
||||
From a full real run (`npm run test:e2e:coverage`) on 2026-02-22:
|
||||
|
||||
- E2E result: `38 passed`
|
||||
- Function coverage: `29.41%` (`2826/9609`)
|
||||
- Loaded source files: `297/313`
|
||||
- Console logs: `68`
|
||||
- Console error-like entries: `6`
|
||||
- Page errors: `0`
|
||||
|
||||
Primary issue groups captured:
|
||||
|
||||
1. `404 Not Found` resource errors (2 occurrences)
|
||||
2. Forced decrypt-timeout warning in diagnostics test (expected by test, 1 occurrence)
|
||||
3. CORS-blocked `.torrent` fetch in webseed playback test (1 occurrence)
|
||||
4. `net::ERR_FAILED` in webseed playback test (1 occurrence)
|
||||
5. WebSocket tracker handshake failure for `wss://tracker.btorrent.xyz/` (1 occurrence)
|
||||
|
||||
Recent high-impact coverage improvements:
|
||||
|
||||
- `js/nostr/commentEvents.js`: `90.91%` (`20/20`)
|
||||
- `js/nostr/reactionEvents.js`: `78.57%` (`11/14`)
|
||||
- `js/nostr/publishHelpers.js`: `26.47%` (`9/34`)
|
||||
|
||||
## Integration Rules For Agents
|
||||
|
||||
1. For `tests/e2e/*.spec.ts`, import from `tests/e2e/helpers/instrumentedTest.ts` so coverage/log capture stays enabled.
|
||||
2. For harness-based tests, use `tests/e2e/helpers/bitvidTestFixture.ts` (it already includes auto-capture).
|
||||
3. Keep coverage runs Chromium-only when collecting percentages (`page.coverage` is Chromium-specific).
|
||||
4. Do not remove `playwright.config.ts` global setup (`tests/e2e/helpers/playwrightCoverageGlobalSetup.ts`), which resets raw coverage artifacts.
|
||||
5. Use `npm run test:e2e:coverage:report` only after a prior coverage-enabled run; it reads existing raw artifacts.
|
||||
|
||||
## How To Improve Coverage
|
||||
|
||||
Priority order for fastest gains:
|
||||
|
||||
1. Add E2E for high-function files currently at `0%` but loaded:
|
||||
- Start with `js/nostr/commentEvents.js`, `js/nostr/reactionEvents.js`, `js/nostr/publishHelpers.js`, `js/nostr/sessionActor.js`.
|
||||
2. Add deterministic DM coverage lane in E2E harness:
|
||||
- Exercise NIP-04 and NIP-44 decrypt/encrypt flows through UI and harness hooks.
|
||||
3. Add upload + publish deep-path tests:
|
||||
- Validate URL-only, magnet-only, and dual-source publish paths with assertions on fallback behavior.
|
||||
4. Add negative-path assertions that currently only surface in console:
|
||||
- Explicitly assert/handle CORS/torrent fetch failures and tracker handshake failures.
|
||||
5. Keep tests routed through test-harness APIs:
|
||||
- Prefer `setTestRelays`, `setSignerDecryptBehavior`, and seeded relay events over live-network dependence.
|
||||
|
||||
## Suggested Quality Gates
|
||||
|
||||
1. Track `functionCoveragePct` trend in CI artifacts per PR.
|
||||
2. Add a soft warning threshold first (for example `>=30%`), then ratchet upward as flaky paths are stabilized.
|
||||
3. Fail PRs only after baseline is stable across multiple runs.
|
||||
@@ -33,6 +33,8 @@
|
||||
"test:e2e:chromium": "npm run build && npx playwright test tests/e2e --project=e2e",
|
||||
"test:e2e:firefox": "npm run build && npx playwright test tests/e2e --project=e2e-firefox",
|
||||
"test:e2e:deterministic": "npm run build && npx playwright test tests/e2e --project=e2e --project=e2e-firefox",
|
||||
"test:e2e:coverage": "npm run build && cross-env PLAYWRIGHT_COVERAGE=1 PLAYWRIGHT_CAPTURE_CONSOLE=1 npx playwright test tests/e2e --project=e2e && node scripts/playwright/generate-function-coverage-report.mjs",
|
||||
"test:e2e:coverage:report": "node scripts/playwright/generate-function-coverage-report.mjs",
|
||||
"test:e2e:extension": "npm run build && node scripts/playwright/run-extension-persistent.mjs",
|
||||
"test:load": "node scripts/agent/load-test.mjs",
|
||||
"test:smoke": "node scripts/agent/smoke-test.mjs",
|
||||
|
||||
@@ -47,6 +47,7 @@ if (ENABLE_EXTENSION_E2E && EXTENSION_PATH) {
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
globalSetup: "./tests/e2e/helpers/playwrightCoverageGlobalSetup.ts",
|
||||
outputDir: "artifacts/test-results",
|
||||
timeout: 60_000,
|
||||
reporter: process.env.CI
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const RAW_DIR = path.join(ROOT_DIR, "artifacts", "playwright-coverage", "raw");
|
||||
const OUTPUT_DIR = path.join(ROOT_DIR, "artifacts", "playwright-coverage");
|
||||
const SOURCE_DIRS = ["js", "torrent"];
|
||||
const SOURCE_EXTENSIONS = new Set([".js"]);
|
||||
const METHOD_KEYWORD_BLOCKLIST = new Set([
|
||||
"if",
|
||||
"for",
|
||||
"while",
|
||||
"switch",
|
||||
"catch",
|
||||
"return",
|
||||
"else",
|
||||
"do",
|
||||
"try",
|
||||
"class",
|
||||
"function",
|
||||
"import",
|
||||
"export",
|
||||
"new",
|
||||
]);
|
||||
|
||||
function normalizeSlashes(value) {
|
||||
return value.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function toRelativeIfInRepo(absPath) {
|
||||
const rel = path.relative(ROOT_DIR, absPath);
|
||||
if (rel.startsWith("..")) return null;
|
||||
return normalizeSlashes(rel);
|
||||
}
|
||||
|
||||
async function walkFiles(startDir) {
|
||||
const entries = await fs.readdir(startDir, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(startDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkFiles(fullPath)));
|
||||
continue;
|
||||
}
|
||||
files.push(fullPath);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function maskStringsAndComments(source) {
|
||||
const out = source.split("");
|
||||
let i = 0;
|
||||
let state = "code";
|
||||
while (i < out.length) {
|
||||
const ch = out[i];
|
||||
const next = out[i + 1];
|
||||
|
||||
if (state === "line-comment") {
|
||||
if (ch === "\n") {
|
||||
state = "code";
|
||||
} else {
|
||||
out[i] = " ";
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "block-comment") {
|
||||
if (ch === "*" && next === "/") {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
state = "code";
|
||||
} else {
|
||||
out[i] = ch === "\n" ? "\n" : " ";
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (state === "single-quote") {
|
||||
if (ch === "\\" && i + 1 < out.length) {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out[i] = ch === "\n" ? "\n" : " ";
|
||||
if (ch === "'") state = "code";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "double-quote") {
|
||||
if (ch === "\\" && i + 1 < out.length) {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out[i] = ch === "\n" ? "\n" : " ";
|
||||
if (ch === "\"") state = "code";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "template") {
|
||||
if (ch === "\\" && i + 1 < out.length) {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "`") {
|
||||
out[i] = " ";
|
||||
i += 1;
|
||||
state = "code";
|
||||
continue;
|
||||
}
|
||||
if (ch === "$" && next === "{") {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
let depth = 1;
|
||||
while (i < out.length && depth > 0) {
|
||||
const c = out[i];
|
||||
if (c === "{") depth += 1;
|
||||
else if (c === "}") depth -= 1;
|
||||
if (depth > 0) out[i] = c === "\n" ? "\n" : " ";
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out[i] = ch === "\n" ? "\n" : " ";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "/" && next === "/") {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
state = "line-comment";
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
out[i] = " ";
|
||||
out[i + 1] = " ";
|
||||
i += 2;
|
||||
state = "block-comment";
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
out[i] = " ";
|
||||
state = "single-quote";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\"") {
|
||||
out[i] = " ";
|
||||
state = "double-quote";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "`") {
|
||||
out[i] = " ";
|
||||
state = "template";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
function estimateStaticFunctionCount(source) {
|
||||
const masked = maskStringsAndComments(source);
|
||||
const seenPositions = new Set();
|
||||
|
||||
const functionPattern = /\bfunction\b\s*[A-Za-z0-9_$]*\s*\(/g;
|
||||
for (const match of masked.matchAll(functionPattern)) {
|
||||
seenPositions.add(match.index);
|
||||
}
|
||||
|
||||
const arrowPattern = /(?:^|[=(:,\[])\s*(?:async\s*)?(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>/gm;
|
||||
for (const match of masked.matchAll(arrowPattern)) {
|
||||
seenPositions.add(match.index);
|
||||
}
|
||||
|
||||
const methodPattern = /(?:^|\n)\s*(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\([^()\n{};]*\)\s*\{/gm;
|
||||
for (const match of masked.matchAll(methodPattern)) {
|
||||
const methodName = match[1];
|
||||
if (METHOD_KEYWORD_BLOCKLIST.has(methodName)) continue;
|
||||
seenPositions.add(match.index);
|
||||
}
|
||||
|
||||
return seenPositions.size;
|
||||
}
|
||||
|
||||
function isLikelyRootCoverageFunction(fn, sourceLength) {
|
||||
if (!fn || !Array.isArray(fn.ranges) || fn.ranges.length === 0) return false;
|
||||
const startOffset = Math.min(...fn.ranges.map((range) => range.startOffset));
|
||||
const endOffset = Math.max(...fn.ranges.map((range) => range.endOffset));
|
||||
const rootSpan = startOffset === 0 && endOffset >= sourceLength - 1;
|
||||
const name = fn.functionName || "";
|
||||
const anonymous = name === "" || name === "(anonymous)";
|
||||
return rootSpan && anonymous;
|
||||
}
|
||||
|
||||
function normalizeCoverageUrlToFile(url) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const pathname = decodeURIComponent(parsed.pathname || "");
|
||||
const rel = pathname.replace(/^\/+/, "");
|
||||
if (!rel) return null;
|
||||
return normalizeSlashes(rel);
|
||||
} catch {
|
||||
if (url.startsWith("/")) return normalizeSlashes(url.replace(/^\/+/, ""));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMessage(text) {
|
||||
return String(text)
|
||||
.replace(/\b\d+\b/g, "<n>")
|
||||
.replace(/\b0x[a-f0-9]+\b/gi, "0x<hex>")
|
||||
.replace(/https?:\/\/\S+/g, "<url>")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
let rawFiles = [];
|
||||
try {
|
||||
rawFiles = (await walkFiles(RAW_DIR)).filter((file) => file.endsWith(".json"));
|
||||
} catch {
|
||||
rawFiles = [];
|
||||
}
|
||||
|
||||
if (rawFiles.length === 0) {
|
||||
const message = `No raw Playwright coverage artifacts found at ${normalizeSlashes(path.relative(ROOT_DIR, RAW_DIR))}. Run with PLAYWRIGHT_COVERAGE=1 first.`;
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const perFileObserved = new Map();
|
||||
const allConsoleLogs = [];
|
||||
const allPageErrors = [];
|
||||
|
||||
for (const rawPath of rawFiles) {
|
||||
const raw = JSON.parse(await fs.readFile(rawPath, "utf8"));
|
||||
const rawRelPath = toRelativeIfInRepo(rawPath) ?? rawPath;
|
||||
|
||||
if (Array.isArray(raw.logs)) {
|
||||
for (const log of raw.logs) {
|
||||
allConsoleLogs.push({
|
||||
test: raw.titlePath || [raw.title || rawRelPath],
|
||||
project: raw.project || "unknown",
|
||||
type: log.type || "log",
|
||||
text: log.text || "",
|
||||
location: log.location || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(raw.pageErrors)) {
|
||||
for (const pageError of raw.pageErrors) {
|
||||
allPageErrors.push({
|
||||
test: raw.titlePath || [raw.title || rawRelPath],
|
||||
project: raw.project || "unknown",
|
||||
message: pageError.message || String(pageError),
|
||||
stack: pageError.stack || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(raw.coverage)) continue;
|
||||
|
||||
for (const entry of raw.coverage) {
|
||||
const relFile = normalizeCoverageUrlToFile(entry.url);
|
||||
if (!relFile) continue;
|
||||
const ext = path.extname(relFile);
|
||||
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
||||
if (!SOURCE_DIRS.some((prefix) => relFile.startsWith(`${prefix}/`))) continue;
|
||||
|
||||
const absFile = path.join(ROOT_DIR, relFile);
|
||||
try {
|
||||
await fs.access(absFile);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = perFileObserved.get(relFile) || {
|
||||
functionMap: new Map(),
|
||||
};
|
||||
|
||||
for (const fn of entry.functions || []) {
|
||||
if (isLikelyRootCoverageFunction(fn, entry.text?.length ?? 0)) continue;
|
||||
const startOffset = Math.min(...fn.ranges.map((range) => range.startOffset));
|
||||
const endOffset = Math.max(...fn.ranges.map((range) => range.endOffset));
|
||||
const key = `${startOffset}:${endOffset}:${fn.functionName || ""}`;
|
||||
const covered = fn.ranges.some((range) => range.count > 0);
|
||||
const previous = existing.functionMap.get(key) || false;
|
||||
existing.functionMap.set(key, previous || covered);
|
||||
}
|
||||
|
||||
perFileObserved.set(relFile, existing);
|
||||
}
|
||||
}
|
||||
|
||||
const sourceFiles = [];
|
||||
for (const sourceDir of SOURCE_DIRS) {
|
||||
const absDir = path.join(ROOT_DIR, sourceDir);
|
||||
let files = [];
|
||||
try {
|
||||
files = await walkFiles(absDir);
|
||||
} catch {
|
||||
files = [];
|
||||
}
|
||||
for (const absPath of files) {
|
||||
const ext = path.extname(absPath);
|
||||
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
||||
const rel = toRelativeIfInRepo(absPath);
|
||||
if (!rel) continue;
|
||||
sourceFiles.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
sourceFiles.sort();
|
||||
|
||||
const fileSummaries = [];
|
||||
let overallCoveredFunctions = 0;
|
||||
let overallEstimatedFunctions = 0;
|
||||
let loadedFileCount = 0;
|
||||
|
||||
for (const relFile of sourceFiles) {
|
||||
const absPath = path.join(ROOT_DIR, relFile);
|
||||
const source = await fs.readFile(absPath, "utf8");
|
||||
const estimatedFunctions = estimateStaticFunctionCount(source);
|
||||
const observed = perFileObserved.get(relFile);
|
||||
const observedTotal = observed ? observed.functionMap.size : 0;
|
||||
const observedCovered = observed
|
||||
? Array.from(observed.functionMap.values()).filter(Boolean).length
|
||||
: 0;
|
||||
const effectiveTotal = Math.max(estimatedFunctions, observedTotal);
|
||||
const effectiveCovered = Math.min(observedCovered, effectiveTotal);
|
||||
const pct = effectiveTotal > 0 ? (effectiveCovered / effectiveTotal) * 100 : 100;
|
||||
|
||||
if (observed) loadedFileCount += 1;
|
||||
overallEstimatedFunctions += effectiveTotal;
|
||||
overallCoveredFunctions += effectiveCovered;
|
||||
|
||||
fileSummaries.push({
|
||||
file: relFile,
|
||||
loadedByPlaywright: Boolean(observed),
|
||||
estimatedFunctions,
|
||||
observedFunctions: observedTotal,
|
||||
coveredFunctions: effectiveCovered,
|
||||
functionCoveragePct: Number(pct.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
fileSummaries.sort((a, b) => a.functionCoveragePct - b.functionCoveragePct);
|
||||
|
||||
const overallCoveragePct =
|
||||
overallEstimatedFunctions > 0
|
||||
? Number(((overallCoveredFunctions / overallEstimatedFunctions) * 100).toFixed(2))
|
||||
: 0;
|
||||
|
||||
const consoleErrorLike = allConsoleLogs.filter((entry) =>
|
||||
["error", "warning", "assert"].includes(String(entry.type).toLowerCase()),
|
||||
);
|
||||
|
||||
const groupedIssues = new Map();
|
||||
for (const entry of consoleErrorLike) {
|
||||
const key = `console:${entry.type}:${normalizeMessage(entry.text)}`;
|
||||
const existing = groupedIssues.get(key) || {
|
||||
kind: "console",
|
||||
level: entry.type,
|
||||
message: normalizeMessage(entry.text),
|
||||
count: 0,
|
||||
samples: [],
|
||||
};
|
||||
existing.count += 1;
|
||||
if (existing.samples.length < 5) {
|
||||
existing.samples.push({
|
||||
test: entry.test.join(" > "),
|
||||
rawMessage: entry.text,
|
||||
});
|
||||
}
|
||||
groupedIssues.set(key, existing);
|
||||
}
|
||||
|
||||
for (const entry of allPageErrors) {
|
||||
const key = `pageerror:${normalizeMessage(entry.message)}`;
|
||||
const existing = groupedIssues.get(key) || {
|
||||
kind: "pageerror",
|
||||
level: "error",
|
||||
message: normalizeMessage(entry.message),
|
||||
count: 0,
|
||||
samples: [],
|
||||
};
|
||||
existing.count += 1;
|
||||
if (existing.samples.length < 5) {
|
||||
existing.samples.push({
|
||||
test: entry.test.join(" > "),
|
||||
rawMessage: entry.message,
|
||||
});
|
||||
}
|
||||
groupedIssues.set(key, existing);
|
||||
}
|
||||
|
||||
const topIssues = Array.from(groupedIssues.values())
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 50);
|
||||
|
||||
const summary = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
scope: {
|
||||
sourceDirs: SOURCE_DIRS,
|
||||
extensionFilter: Array.from(SOURCE_EXTENSIONS),
|
||||
},
|
||||
coverage: {
|
||||
totalSourceFiles: sourceFiles.length,
|
||||
loadedSourceFiles: loadedFileCount,
|
||||
estimatedFunctionCount: overallEstimatedFunctions,
|
||||
coveredFunctionCount: overallCoveredFunctions,
|
||||
functionCoveragePct: overallCoveragePct,
|
||||
},
|
||||
testArtifacts: {
|
||||
rawArtifactCount: rawFiles.length,
|
||||
consoleLogCount: allConsoleLogs.length,
|
||||
consoleErrorLikeCount: consoleErrorLike.length,
|
||||
pageErrorCount: allPageErrors.length,
|
||||
},
|
||||
topIssues,
|
||||
files: fileSummaries,
|
||||
};
|
||||
|
||||
const lowCoverageFiles = fileSummaries
|
||||
.filter((item) => item.estimatedFunctions >= 5)
|
||||
.slice(0, 25);
|
||||
|
||||
const summaryMd = [
|
||||
"# Playwright Function Coverage Summary",
|
||||
"",
|
||||
`Generated: ${summary.generatedAt}`,
|
||||
"",
|
||||
`Overall function coverage: **${overallCoveragePct}%**`,
|
||||
"",
|
||||
`Covered functions: ${overallCoveredFunctions}/${overallEstimatedFunctions}`,
|
||||
"",
|
||||
`Source files loaded during Playwright run: ${loadedFileCount}/${sourceFiles.length}`,
|
||||
"",
|
||||
"## Console/Page Error Signals",
|
||||
"",
|
||||
`Console logs captured: ${allConsoleLogs.length}`,
|
||||
"",
|
||||
`Console error-like entries: ${consoleErrorLike.length}`,
|
||||
"",
|
||||
`Page errors: ${allPageErrors.length}`,
|
||||
"",
|
||||
"## Highest-Risk Coverage Gaps",
|
||||
"",
|
||||
"| File | Coverage % | Covered/Total | Loaded |",
|
||||
"| --- | ---: | ---: | :---: |",
|
||||
...lowCoverageFiles.map(
|
||||
(item) =>
|
||||
`| ${item.file} | ${item.functionCoveragePct}% | ${item.coveredFunctions}/${item.estimatedFunctions} | ${item.loadedByPlaywright ? "yes" : "no"} |`,
|
||||
),
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const issueLogLines = [
|
||||
`Generated: ${summary.generatedAt}`,
|
||||
`Console logs: ${allConsoleLogs.length}`,
|
||||
`Console error-like: ${consoleErrorLike.length}`,
|
||||
`Page errors: ${allPageErrors.length}`,
|
||||
"",
|
||||
...topIssues.map((issue, index) => {
|
||||
const samples = issue.samples
|
||||
.map((sample) => ` - ${sample.test}: ${sample.rawMessage}`)
|
||||
.join("\n");
|
||||
return `${index + 1}. [${issue.kind}/${issue.level}] x${issue.count}\n ${issue.message}\n${samples}`;
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "function-coverage-summary.json"),
|
||||
JSON.stringify(summary, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "function-coverage-summary.md"), summaryMd, "utf8");
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "console-issues.log"), issueLogLines, "utf8");
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "console-log-summary.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
generatedAt: summary.generatedAt,
|
||||
consoleLogs: allConsoleLogs,
|
||||
pageErrors: allPageErrors,
|
||||
topIssues,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
console.log(`Playwright function coverage: ${overallCoveragePct}% (${overallCoveredFunctions}/${overallEstimatedFunctions})`);
|
||||
console.log(`Loaded source files: ${loadedFileCount}/${sourceFiles.length}`);
|
||||
console.log(`Console/page issues: ${topIssues.length} grouped signals`);
|
||||
console.log(`Summary JSON: artifacts/playwright-coverage/function-coverage-summary.json`);
|
||||
console.log(`Summary MD: artifacts/playwright-coverage/function-coverage-summary.md`);
|
||||
console.log(`Issue log: artifacts/playwright-coverage/console-issues.log`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
|
||||
test("reload after deploy upgrade picks latest hashed bundle without manual cache clear", async ({
|
||||
page,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { test as base, expect, type Page } from "@playwright/test";
|
||||
import { startRelay } from "../../../scripts/agent/simple-relay.mjs";
|
||||
import { finalizeEvent, getPublicKey } from "nostr-tools";
|
||||
import { WebSocket } from "ws";
|
||||
import { attachCoverageAndConsoleCapture } from "./playwrightCoverageInstrumentation";
|
||||
|
||||
// Polyfill WebSocket for node environment
|
||||
if (!global.WebSocket) {
|
||||
@@ -143,6 +144,7 @@ async function loginWithTestKey(page: Page) {
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
type BitvidFixtures = {
|
||||
_coverageAndConsoleCapture: void;
|
||||
relay: Awaited<ReturnType<typeof startRelay>>;
|
||||
seedEvent: (video: TestVideoEvent) => Promise<any>;
|
||||
seedRawEvent: (event: any) => Promise<any>;
|
||||
@@ -175,6 +177,15 @@ type BitvidFixtures = {
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
export const test = base.extend<BitvidFixtures>({
|
||||
_coverageAndConsoleCapture: [
|
||||
async ({ page, browserName }, use, testInfo) => {
|
||||
const stopCapture = await attachCoverageAndConsoleCapture(page, testInfo, browserName);
|
||||
await use();
|
||||
await stopCapture();
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
|
||||
relay: [
|
||||
async ({}, use, testInfo) => {
|
||||
// Avoid port collisions when running in parallel workers
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { attachCoverageAndConsoleCapture } from "./playwrightCoverageInstrumentation";
|
||||
|
||||
export const test = base.extend({
|
||||
_coverageAndConsoleCapture: [
|
||||
async ({ page, browserName }, use, testInfo) => {
|
||||
const stopCapture = await attachCoverageAndConsoleCapture(page, testInfo, browserName);
|
||||
await use();
|
||||
await stopCapture();
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
});
|
||||
|
||||
export { expect };
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { FullConfig } from "@playwright/test";
|
||||
import { prepareCoverageArtifactsDir } from "./playwrightCoverageInstrumentation";
|
||||
|
||||
export default async function globalSetup(_config: FullConfig): Promise<void> {
|
||||
await prepareCoverageArtifactsDir();
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page, TestInfo } from "@playwright/test";
|
||||
|
||||
type ConsoleEntry = {
|
||||
type: string;
|
||||
text: string;
|
||||
location?: {
|
||||
url?: string;
|
||||
lineNumber?: number;
|
||||
columnNumber?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type PageErrorEntry = {
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
type CoverageEntry = {
|
||||
url: string;
|
||||
text: string;
|
||||
functions: Array<{
|
||||
functionName: string;
|
||||
isBlockCoverage: boolean;
|
||||
ranges: Array<{ startOffset: number; endOffset: number; count: number }>;
|
||||
}>;
|
||||
};
|
||||
|
||||
function sanitizeSegment(value: string): string {
|
||||
return value
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 120);
|
||||
}
|
||||
|
||||
function getRawArtifactDir(): string {
|
||||
return path.join(process.cwd(), "artifacts", "playwright-coverage", "raw");
|
||||
}
|
||||
|
||||
export async function prepareCoverageArtifactsDir(): Promise<void> {
|
||||
if (process.env.PLAYWRIGHT_COVERAGE !== "1") return;
|
||||
const dir = getRawArtifactDir();
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export async function attachCoverageAndConsoleCapture(
|
||||
page: Page,
|
||||
testInfo: TestInfo,
|
||||
browserName: string,
|
||||
): Promise<() => Promise<void>> {
|
||||
const captureCoverage = process.env.PLAYWRIGHT_COVERAGE === "1";
|
||||
const captureConsole = process.env.PLAYWRIGHT_CAPTURE_CONSOLE === "1" || captureCoverage;
|
||||
const rawDir = getRawArtifactDir();
|
||||
|
||||
const consoleEntries: ConsoleEntry[] = [];
|
||||
const pageErrors: PageErrorEntry[] = [];
|
||||
let coverageStarted = false;
|
||||
|
||||
const onConsole = (message: any) => {
|
||||
consoleEntries.push({
|
||||
type: message.type(),
|
||||
text: message.text(),
|
||||
location: message.location(),
|
||||
});
|
||||
};
|
||||
|
||||
const onPageError = (error: Error) => {
|
||||
pageErrors.push({
|
||||
message: error?.message || String(error),
|
||||
stack: error?.stack,
|
||||
});
|
||||
};
|
||||
|
||||
if (captureConsole) {
|
||||
page.on("console", onConsole);
|
||||
page.on("pageerror", onPageError);
|
||||
}
|
||||
|
||||
if (captureCoverage && browserName === "chromium") {
|
||||
await page.coverage.startJSCoverage({
|
||||
resetOnNavigation: false,
|
||||
reportAnonymousScripts: false,
|
||||
});
|
||||
coverageStarted = true;
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const testId = sanitizeSegment(
|
||||
`${testInfo.project.name}-${testInfo.titlePath.join(" ")}-retry-${testInfo.retry}`,
|
||||
);
|
||||
|
||||
page.off("console", onConsole);
|
||||
page.off("pageerror", onPageError);
|
||||
|
||||
let coverage: CoverageEntry[] = [];
|
||||
if (coverageStarted) {
|
||||
coverage = (await page.coverage.stopJSCoverage()) as CoverageEntry[];
|
||||
}
|
||||
|
||||
if (!captureCoverage && !captureConsole) return;
|
||||
|
||||
await fs.mkdir(rawDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(rawDir, `${testId}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
timestamp,
|
||||
project: testInfo.project.name,
|
||||
title: testInfo.title,
|
||||
titlePath: testInfo.titlePath,
|
||||
file: testInfo.file,
|
||||
retry: testInfo.retry,
|
||||
status: testInfo.status,
|
||||
expectedStatus: testInfo.expectedStatus,
|
||||
browserName,
|
||||
logs: consoleEntries,
|
||||
pageErrors,
|
||||
coverage,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
|
||||
const HEX_A = "a".repeat(64);
|
||||
const HEX_B = "b".repeat(64);
|
||||
const HEX_C = "c".repeat(64);
|
||||
const HEX_D = "d".repeat(64);
|
||||
|
||||
test.describe("nostr runtime coverage (deterministic)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/docs/kitchen-sink.html", { waitUntil: "networkidle" });
|
||||
});
|
||||
|
||||
test("exercises publish helper signing + rebroadcast guard paths", async ({
|
||||
page,
|
||||
}) => {
|
||||
const result = await page.evaluate(async ({ actorA, actorB }) => {
|
||||
const {
|
||||
deriveRebroadcastScope,
|
||||
deriveRebroadcastBucketIndex,
|
||||
rememberRebroadcastAttempt,
|
||||
hasRecentRebroadcastAttempt,
|
||||
getRebroadcastCooldownState,
|
||||
signAndPublishEvent,
|
||||
} = await import("/js/nostr/publishHelpers.js");
|
||||
|
||||
const scope = deriveRebroadcastScope(actorA, "f".repeat(64));
|
||||
const bucket = deriveRebroadcastBucketIndex(1_700_000_000);
|
||||
rememberRebroadcastAttempt(scope, bucket);
|
||||
const hasRecent = hasRecentRebroadcastAttempt(scope, bucket);
|
||||
const cooldown = getRebroadcastCooldownState(scope);
|
||||
|
||||
const publishCalls = [];
|
||||
const client = {
|
||||
pubkey: actorA,
|
||||
writeRelays: ["wss://relay.one", "wss://relay.two"],
|
||||
relays: ["wss://relay.one"],
|
||||
sessionActor: null,
|
||||
ensureSessionActor: async () => {
|
||||
client.sessionActor = {
|
||||
pubkey: actorB,
|
||||
privateKey: "1".repeat(64),
|
||||
source: "nip46",
|
||||
};
|
||||
return actorB;
|
||||
},
|
||||
pool: {
|
||||
publish(relays, event) {
|
||||
publishCalls.push({ relays, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
Promise.resolve().then(() => handler());
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const activeEvent = {
|
||||
kind: 1,
|
||||
pubkey: actorA,
|
||||
created_at: 123,
|
||||
tags: [],
|
||||
content: "active-signer",
|
||||
};
|
||||
|
||||
const activeResult = await signAndPublishEvent({
|
||||
client,
|
||||
event: activeEvent,
|
||||
options: { context: "test-active", logName: "active" },
|
||||
resolveActiveSigner: () => ({
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: "id-active",
|
||||
sig: "sig-active",
|
||||
}),
|
||||
}),
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: () => {
|
||||
throw new Error("unexpected-session-fallback");
|
||||
},
|
||||
});
|
||||
|
||||
let fallbackSignCalls = 0;
|
||||
const fallbackEvent = {
|
||||
kind: 1,
|
||||
pubkey: actorB,
|
||||
created_at: 456,
|
||||
tags: [],
|
||||
content: "session-signer",
|
||||
};
|
||||
|
||||
const fallbackResult = await signAndPublishEvent({
|
||||
client,
|
||||
event: fallbackEvent,
|
||||
options: { context: "test-fallback", logName: "fallback" },
|
||||
resolveActiveSigner: () => null,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: (event) => {
|
||||
fallbackSignCalls += 1;
|
||||
return {
|
||||
...event,
|
||||
id: "id-session",
|
||||
sig: "sig-session",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
hasRecent,
|
||||
cooldownRemainingPositive: Number(cooldown?.remainingMs || 0) > 0,
|
||||
activeAcceptedCount: activeResult.summary.accepted.length,
|
||||
activeSignerPubkey: activeResult.signerPubkey,
|
||||
fallbackAcceptedCount: fallbackResult.summary.accepted.length,
|
||||
fallbackSignerPubkey: fallbackResult.signerPubkey,
|
||||
fallbackSignCalls,
|
||||
publishCallCount: publishCalls.length,
|
||||
};
|
||||
}, { actorA: HEX_A, actorB: HEX_B });
|
||||
|
||||
expect(result.hasRecent).toBe(true);
|
||||
expect(result.cooldownRemainingPositive).toBe(true);
|
||||
expect(result.activeAcceptedCount).toBeGreaterThan(0);
|
||||
expect(result.activeSignerPubkey).toBe(HEX_A);
|
||||
expect(result.fallbackAcceptedCount).toBeGreaterThan(0);
|
||||
expect(result.fallbackSignerPubkey).toBe(HEX_B);
|
||||
expect(result.fallbackSignCalls).toBe(1);
|
||||
expect(result.publishCallCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("exercises comment publish/list/subscribe and session-actor publish guard", async ({
|
||||
page,
|
||||
}) => {
|
||||
const result = await page.evaluate(async ({ actor }) => {
|
||||
const {
|
||||
COMMENT_EVENT_KIND,
|
||||
publishComment,
|
||||
listVideoComments,
|
||||
subscribeVideoComments,
|
||||
} = await import("/js/nostr/commentEvents.js");
|
||||
|
||||
const relay = "wss://relay.comments";
|
||||
const videoEventId = "e".repeat(64);
|
||||
const listCalls = [];
|
||||
const publishedEvents = [];
|
||||
let subEventHandler = null;
|
||||
let unsubCalled = false;
|
||||
|
||||
const client = {
|
||||
pubkey: actor,
|
||||
relays: [relay],
|
||||
ensureExtensionPermissions: async () => ({ ok: true }),
|
||||
pool: {
|
||||
publish(relays, event) {
|
||||
publishedEvents.push({ relays, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
Promise.resolve().then(() => handler());
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
async list(relays, filters) {
|
||||
listCalls.push({ relays, filters });
|
||||
const match = publishedEvents[0]?.event || null;
|
||||
const invalid = {
|
||||
id: "invalid-comment-id",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
created_at: 1,
|
||||
tags: [["e", "not-the-video"]],
|
||||
content: "invalid",
|
||||
pubkey: actor,
|
||||
sig: "s",
|
||||
};
|
||||
return [match, invalid].filter(Boolean);
|
||||
},
|
||||
sub(relays, filters) {
|
||||
listCalls.push({ relays, filters, type: "sub" });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "event") {
|
||||
subEventHandler = handler;
|
||||
}
|
||||
},
|
||||
unsub() {
|
||||
unsubCalled = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const signer = {
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: "comment-event-id",
|
||||
sig: "comment-event-sig",
|
||||
}),
|
||||
};
|
||||
|
||||
const publishResult = await publishComment(
|
||||
client,
|
||||
{ videoEventPointer: ["e", videoEventId, relay] },
|
||||
{
|
||||
content: { text: "coverage comment" },
|
||||
additionalTags: [["client", "playwright"]],
|
||||
},
|
||||
{
|
||||
resolveActiveSigner: () => signer,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
||||
},
|
||||
);
|
||||
|
||||
const listedFirst = await listVideoComments(
|
||||
client,
|
||||
{ videoEventPointer: ["e", videoEventId, relay] },
|
||||
{ forceRefresh: true, relays: [relay] },
|
||||
);
|
||||
|
||||
const listCallCountAfterFirst = listCalls.length;
|
||||
const listedSecond = await listVideoComments(
|
||||
client,
|
||||
{ videoEventPointer: ["e", videoEventId, relay] },
|
||||
{ relays: [relay] },
|
||||
);
|
||||
const listCallCountAfterSecond = listCalls.length;
|
||||
|
||||
const seenIds = [];
|
||||
const unsubscribe = subscribeVideoComments(
|
||||
client,
|
||||
{ videoEventPointer: ["e", videoEventId, relay] },
|
||||
{
|
||||
relays: [relay],
|
||||
onEvent: (event) => {
|
||||
seenIds.push(event.id);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
if (typeof subEventHandler === "function") {
|
||||
subEventHandler(publishResult.event);
|
||||
subEventHandler({
|
||||
id: "mismatch",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
created_at: 2,
|
||||
tags: [["e", "not-the-video"]],
|
||||
pubkey: actor,
|
||||
content: "skip",
|
||||
sig: "s",
|
||||
});
|
||||
}
|
||||
unsubscribe();
|
||||
|
||||
const blockedClient = {
|
||||
pubkey: actor,
|
||||
sessionActor: { pubkey: actor, source: "nip46" },
|
||||
relays: [relay],
|
||||
pool: client.pool,
|
||||
};
|
||||
const blockedPublishResult = await publishComment(
|
||||
blockedClient,
|
||||
{ videoEventPointer: ["e", videoEventId, relay] },
|
||||
{ content: "blocked" },
|
||||
{
|
||||
resolveActiveSigner: () => signer,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
publishOk: publishResult.ok,
|
||||
publishKind: publishResult.event?.kind,
|
||||
listedFirstCount: listedFirst.length,
|
||||
listedSecondCount: listedSecond.length,
|
||||
usedCacheOnSecondList:
|
||||
listCallCountAfterSecond === listCallCountAfterFirst,
|
||||
seenIds,
|
||||
unsubCalled,
|
||||
blockedError: blockedPublishResult.error,
|
||||
};
|
||||
}, { actor: HEX_C });
|
||||
|
||||
expect(result.publishOk).toBe(true);
|
||||
expect(result.publishKind).toBeGreaterThan(0);
|
||||
expect(result.listedFirstCount).toBe(1);
|
||||
expect(result.listedSecondCount).toBe(1);
|
||||
expect(result.usedCacheOnSecondList).toBe(true);
|
||||
expect(result.seenIds).toEqual(["comment-event-id"]);
|
||||
expect(result.unsubCalled).toBe(true);
|
||||
expect(result.blockedError).toBe("session-actor-publish-blocked");
|
||||
});
|
||||
|
||||
test("exercises reaction publish/list for active signer and session fallback", async ({
|
||||
page,
|
||||
}) => {
|
||||
const result = await page.evaluate(async ({ actorA, actorB }) => {
|
||||
const { publishVideoReaction, listVideoReactions } = await import(
|
||||
"/js/nostr/reactionEvents.js"
|
||||
);
|
||||
|
||||
const relay = "wss://relay.reactions";
|
||||
const videoEventId = "9".repeat(64);
|
||||
const listCalls = [];
|
||||
const publishedEvents = [];
|
||||
let fallbackSignCalls = 0;
|
||||
|
||||
const client = {
|
||||
pubkey: actorA,
|
||||
relays: [relay],
|
||||
sessionActor: null,
|
||||
ensureExtensionPermissions: async () => ({ ok: true }),
|
||||
ensureSessionActor: async (forceNew = false) => {
|
||||
if (!client.sessionActor || forceNew) {
|
||||
client.sessionActor = {
|
||||
pubkey: forceNew ? actorB : actorA,
|
||||
privateKey: "2".repeat(64),
|
||||
source: "nip46",
|
||||
};
|
||||
}
|
||||
return client.sessionActor.pubkey;
|
||||
},
|
||||
pool: {
|
||||
publish(relays, event) {
|
||||
publishedEvents.push({ relays, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
Promise.resolve().then(() => handler());
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
async list(relays, filters) {
|
||||
listCalls.push({ relays, filters });
|
||||
const primary = publishedEvents[0]?.event || null;
|
||||
const invalid = {
|
||||
id: "invalid-reaction",
|
||||
kind: 7,
|
||||
created_at: 2,
|
||||
tags: [["e", "deadbeef"]],
|
||||
pubkey: actorA,
|
||||
content: "+",
|
||||
sig: "s",
|
||||
};
|
||||
return [primary, invalid].filter(Boolean);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const activeResult = await publishVideoReaction(
|
||||
client,
|
||||
["e", videoEventId, relay],
|
||||
{ content: "+" },
|
||||
{
|
||||
resolveActiveSigner: () => ({
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: "reaction-active-id",
|
||||
sig: "reaction-active-sig",
|
||||
}),
|
||||
}),
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: () => {
|
||||
throw new Error("unexpected-session-fallback");
|
||||
},
|
||||
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
||||
},
|
||||
);
|
||||
|
||||
const reactionsFirst = await listVideoReactions(
|
||||
client,
|
||||
{ id: videoEventId },
|
||||
{ forceRefresh: true, relays: [relay] },
|
||||
);
|
||||
const listCountAfterFirst = listCalls.length;
|
||||
const reactionsSecond = await listVideoReactions(
|
||||
client,
|
||||
{ id: videoEventId },
|
||||
{ relays: [relay] },
|
||||
);
|
||||
const listCountAfterSecond = listCalls.length;
|
||||
|
||||
client.pubkey = actorA;
|
||||
client.sessionActor = { pubkey: actorB, privateKey: "3".repeat(64), source: "nip46" };
|
||||
const fallbackResult = await publishVideoReaction(
|
||||
client,
|
||||
["e", videoEventId, relay],
|
||||
{ content: "🔥" },
|
||||
{
|
||||
resolveActiveSigner: () => null,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: (event) => {
|
||||
fallbackSignCalls += 1;
|
||||
return {
|
||||
...event,
|
||||
id: "reaction-session-id",
|
||||
sig: "reaction-session-sig",
|
||||
};
|
||||
},
|
||||
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
activeOk: activeResult.ok,
|
||||
activeAccepted: activeResult.acceptedRelays.length,
|
||||
reactionsFirstCount: reactionsFirst.length,
|
||||
reactionsSecondCount: reactionsSecond.length,
|
||||
usedCacheOnSecondList: listCountAfterSecond === listCountAfterFirst,
|
||||
fallbackOk: fallbackResult.ok,
|
||||
fallbackSignCalls,
|
||||
};
|
||||
}, { actorA: HEX_A, actorB: HEX_D });
|
||||
|
||||
expect(result.activeOk).toBe(true);
|
||||
expect(result.activeAccepted).toBeGreaterThan(0);
|
||||
expect(result.reactionsFirstCount).toBe(1);
|
||||
expect(result.reactionsSecondCount).toBe(1);
|
||||
expect(result.usedCacheOnSecondList).toBe(true);
|
||||
expect(result.fallbackOk).toBe(true);
|
||||
expect(result.fallbackSignCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
import {
|
||||
getPanelMetrics,
|
||||
getPanelWithTriggerMetrics,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from './helpers/instrumentedTest';
|
||||
import { startRelay } from '../../scripts/agent/simple-relay.mjs';
|
||||
import { WebSocket } from 'ws';
|
||||
import { finalizeEvent, getPublicKey, nip19 } from 'nostr-tools';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
import { applyReducedMotion, failOnConsoleErrors } from "./helpers/uiTestUtils";
|
||||
|
||||
test.describe("toast service in full DOM", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
import {
|
||||
applyReducedMotion,
|
||||
ensureTestAssets,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test } from "./helpers/instrumentedTest";
|
||||
import { applyReducedMotion, failOnConsoleErrors } from "./helpers/uiTestUtils";
|
||||
|
||||
test.describe("video modal share button", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect } from "./helpers/instrumentedTest";
|
||||
|
||||
test.describe("Webseed Stream Playback", () => {
|
||||
const VIDEO_URL =
|
||||
|
||||
Reference in new issue
Block a user