staklink 0.3.24 → 0.3.26

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.
@@ -34824,7 +34824,7 @@ var SSEManager = class {
34824
34824
  var sseManager = new SSEManager();
34825
34825
 
34826
34826
  // src/proxy/version.ts
34827
- var VERSION = "0.3.24";
34827
+ var VERSION = "0.3.26";
34828
34828
 
34829
34829
  // node_modules/uuid/dist/esm/stringify.js
34830
34830
  var byteToHex = [];
@@ -80367,6 +80367,9 @@ function gitleaksProtect() {
80367
80367
  var import_child_process4 = require("child_process");
80368
80368
  var import_util10 = require("util");
80369
80369
  var execAsync = (0, import_util10.promisify)(import_child_process4.exec);
80370
+ function sanitizeGitHubText(text2) {
80371
+ return text2.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$");
80372
+ }
80370
80373
  var GitHubCLI = class {
80371
80374
  token;
80372
80375
  branch;
@@ -80474,10 +80477,10 @@ Stderr: ${error82.stderr}`
80474
80477
  "--head",
80475
80478
  this.branch,
80476
80479
  "--title",
80477
- `"${options.title}"`
80480
+ `"${sanitizeGitHubText(options.title)}"`
80478
80481
  ];
80479
80482
  if (options.body) {
80480
- args.push("--body", `"${options.body}"`);
80483
+ args.push("--body", `"${sanitizeGitHubText(options.body)}"`);
80481
80484
  }
80482
80485
  if (options.base) {
80483
80486
  args.push("--base", options.base);
@@ -80619,10 +80622,10 @@ Stderr: ${error82.stderr}`
80619
80622
  args.push("--delete-branch");
80620
80623
  }
80621
80624
  if (commitTitle) {
80622
- args.push("--subject", `"${commitTitle}"`);
80625
+ args.push("--subject", `"${sanitizeGitHubText(commitTitle)}"`);
80623
80626
  }
80624
80627
  if (commitBody) {
80625
- args.push("--body", `"${commitBody}"`);
80628
+ args.push("--body", `"${sanitizeGitHubText(commitBody)}"`);
80626
80629
  }
80627
80630
  try {
80628
80631
  const { stdout } = await this.execGH(args.join(" "));
@@ -80676,7 +80679,9 @@ Stderr: ${error82.stderr}`
80676
80679
  */
80677
80680
  async addComment(prNumber, comment) {
80678
80681
  try {
80679
- await this.execGH(`pr comment ${prNumber} --body "${comment}"`);
80682
+ await this.execGH(
80683
+ `pr comment ${prNumber} --body "${sanitizeGitHubText(comment)}"`
80684
+ );
80680
80685
  } catch (error82) {
80681
80686
  throw new Error(`Failed to add comment to PR #${prNumber}: ${error82}`);
80682
80687
  }
@@ -80687,10 +80692,10 @@ Stderr: ${error82.stderr}`
80687
80692
  async updatePR(prNumber, updates) {
80688
80693
  const args = ["pr", "edit", prNumber.toString()];
80689
80694
  if (updates.title) {
80690
- args.push("--title", `"${updates.title}"`);
80695
+ args.push("--title", `"${sanitizeGitHubText(updates.title)}"`);
80691
80696
  }
80692
80697
  if (updates.body) {
80693
- args.push("--body", `"${updates.body}"`);
80698
+ args.push("--body", `"${sanitizeGitHubText(updates.body)}"`);
80694
80699
  }
80695
80700
  try {
80696
80701
  await this.execGH(args.join(" "));
@@ -80751,6 +80756,189 @@ Stderr: ${error82.stderr}`
80751
80756
  // src/proxy/playwright.ts
80752
80757
  var fs10 = __toESM(require("fs/promises"), 1);
80753
80758
  var path9 = __toESM(require("path"), 1);
80759
+ var DEFAULT_CONFIG = `import { defineConfig } from "@playwright/test";
80760
+ export default defineConfig({
80761
+ timeout: 60000,
80762
+ workers: 1,
80763
+ fullyParallel: false,
80764
+ reporter: [
80765
+ ["list"],
80766
+ ["./timestamp-reporter.ts"],
80767
+ ],
80768
+ use: {
80769
+ video: "on",
80770
+ headless: true,
80771
+ browserName: "chromium",
80772
+ trace: "on-first-retry",
80773
+ },
80774
+ });
80775
+ `;
80776
+ var TIMESTAMP_REPORTER_CODE = `import {
80777
+ Reporter,
80778
+ TestCase,
80779
+ TestResult,
80780
+ TestStep,
80781
+ FullConfig,
80782
+ Suite,
80783
+ FullResult,
80784
+ } from "@playwright/test/reporter";
80785
+ import * as fs from "fs";
80786
+ import * as path from "path";
80787
+
80788
+ interface TimestampEntry {
80789
+ title: string;
80790
+ category: string;
80791
+ timestamp: string;
80792
+ relativeMs: number;
80793
+ duration: number;
80794
+ location: {
80795
+ file: string;
80796
+ line: number;
80797
+ column: number;
80798
+ };
80799
+ stepPath: string[];
80800
+ sourceCode?: string;
80801
+ error?: {
80802
+ message: string;
80803
+ stack?: string;
80804
+ };
80805
+ }
80806
+
80807
+ class TimestampReporter implements Reporter {
80808
+ private logs: TimestampEntry[] = [];
80809
+ private testStartTime: number = 0;
80810
+ private outputDir: string = "test-results";
80811
+ private sourceCache: Map<string, string[]> = new Map();
80812
+
80813
+ onBegin(config: FullConfig, suite: Suite) {
80814
+ this.outputDir = config.projects[0]?.outputDir || "test-results";
80815
+ console.log("\\n\u{1F3AC} Timestamp Reporter: Recording action timestamps\\n");
80816
+ }
80817
+
80818
+ private readSourceLine(filePath: string, lineNumber: number): string | undefined {
80819
+ try {
80820
+ // Check cache first
80821
+ if (!this.sourceCache.has(filePath)) {
80822
+ const content = fs.readFileSync(filePath, "utf-8");
80823
+ this.sourceCache.set(filePath, content.split("\\n"));
80824
+ }
80825
+
80826
+ const lines = this.sourceCache.get(filePath);
80827
+ if (lines && lineNumber > 0 && lineNumber <= lines.length) {
80828
+ return lines[lineNumber - 1].trim();
80829
+ }
80830
+ } catch (error) {
80831
+ console.error(\`Error reading source file \${filePath}:\`, error);
80832
+ }
80833
+ return undefined;
80834
+ }
80835
+
80836
+ onTestBegin(test: TestCase, result: TestResult) {
80837
+ this.testStartTime = Date.now();
80838
+ console.log(\`\\n\u{1F4DD} [\${new Date().toISOString()}] Test started: \${test.title}\`);
80839
+ }
80840
+
80841
+ onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
80842
+ // Only log steps that have a location (actual test code)
80843
+ if (!step.location) return;
80844
+
80845
+ const timestamp = new Date().toISOString();
80846
+ const relativeMs = Date.now() - this.testStartTime;
80847
+ const indent = " ".repeat(step.titlePath().length - 1);
80848
+ const sourceCode = this.readSourceLine(step.location.file, step.location.line);
80849
+
80850
+ console.log(
80851
+ \`\${indent}\u23F1\uFE0F [\${timestamp}] +\${relativeMs}ms - \${sourceCode || step.title}\`
80852
+ );
80853
+ }
80854
+
80855
+ onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
80856
+ // Only log steps that have a location (actual test code)
80857
+ if (!step.location) return;
80858
+
80859
+ const timestamp = new Date().toISOString();
80860
+ const relativeMs = Date.now() - this.testStartTime;
80861
+
80862
+ // Read the actual source code line
80863
+ const sourceCode = this.readSourceLine(step.location.file, step.location.line);
80864
+
80865
+ const entry: TimestampEntry = {
80866
+ title: step.title,
80867
+ category: step.category,
80868
+ timestamp: timestamp,
80869
+ relativeMs: relativeMs,
80870
+ duration: step.duration,
80871
+ stepPath: step.titlePath(),
80872
+ location: {
80873
+ file: step.location.file,
80874
+ line: step.location.line,
80875
+ column: step.location.column,
80876
+ },
80877
+ sourceCode: sourceCode,
80878
+ };
80879
+
80880
+ // Add error if present
80881
+ if (step.error) {
80882
+ entry.error = {
80883
+ message: step.error.message || "Unknown error",
80884
+ stack: step.error.stack,
80885
+ };
80886
+ }
80887
+
80888
+ this.logs.push(entry);
80889
+
80890
+ const indent = " ".repeat(step.titlePath().length - 1);
80891
+ const status = step.error ? "\u274C" : "\u2705";
80892
+ console.log(
80893
+ \`\${indent}\${status} [\${timestamp}] \${sourceCode || step.title} (\${step.duration}ms)\`
80894
+ );
80895
+ }
80896
+
80897
+ onTestEnd(test: TestCase, result: TestResult) {
80898
+ const videoAttachment = result.attachments.find((a) => a.name === "video");
80899
+ const videoPath = videoAttachment?.path;
80900
+
80901
+ const output = {
80902
+ testTitle: test.title,
80903
+ testId: test.id,
80904
+ status: result.status,
80905
+ duration: result.duration,
80906
+ startTime: new Date(this.testStartTime).toISOString(),
80907
+ videoPath: videoPath || null,
80908
+ actions: this.logs,
80909
+ };
80910
+
80911
+ // Write to test-results directory
80912
+ const sanitizedTitle = test.title
80913
+ .replace(/[^a-z0-9]/gi, "-")
80914
+ .toLowerCase();
80915
+ const outputPath = path.join(
80916
+ this.outputDir,
80917
+ \`timestamps-\${sanitizedTitle}.json\`
80918
+ );
80919
+
80920
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
80921
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
80922
+
80923
+ console.log(\`\\n\u{1F4BE} Timestamps saved to: \${outputPath}\`);
80924
+ if (videoPath) {
80925
+ console.log(\`\u{1F4F9} Video path: \${videoPath}\`);
80926
+ }
80927
+ console.log("");
80928
+
80929
+ // Reset logs for next test
80930
+ this.logs = [];
80931
+ // Clear source cache to free memory
80932
+ this.sourceCache.clear();
80933
+ }
80934
+
80935
+ onEnd(result: FullResult) {
80936
+ console.log(\`\\n\u2705 Test run complete! Status: \${result.status}\\n\`);
80937
+ }
80938
+ }
80939
+
80940
+ export default TimestampReporter;
80941
+ `;
80754
80942
  async function findRepositoryLocation(repoName) {
80755
80943
  const repoLocation = await findRepoLocation(repoName);
80756
80944
  if (!repoLocation) {
@@ -80785,12 +80973,39 @@ async function findPlaywrightConfig(repoLocation) {
80785
80973
  }
80786
80974
  async function createPlaywrightConfig(repoLocation) {
80787
80975
  const configPath = path9.join(repoLocation, "playwright.config.ts");
80788
- const defaultConfig = `import { defineConfig } from "@playwright/test";
80976
+ await fs10.writeFile(configPath, DEFAULT_CONFIG);
80977
+ console.log(`Created playwright config at ${configPath}`);
80978
+ return configPath;
80979
+ }
80980
+ async function createTimestampReporter(repoLocation) {
80981
+ const reporterPath = path9.join(repoLocation, "timestamp-reporter.ts");
80982
+ await fs10.writeFile(reporterPath, TIMESTAMP_REPORTER_CODE);
80983
+ console.log(`Created timestamp reporter at ${reporterPath}`);
80984
+ return reporterPath;
80985
+ }
80986
+ async function setupPlaywrightConfig(repoLocation) {
80987
+ let configPath = await findPlaywrightConfig(repoLocation);
80988
+ let wasCreated = false;
80989
+ let originalContent = null;
80990
+ let wasModified = false;
80991
+ const reporterPath = await createTimestampReporter(repoLocation);
80992
+ if (!configPath) {
80993
+ configPath = await createPlaywrightConfig(repoLocation);
80994
+ wasCreated = true;
80995
+ console.log("Created new playwright config");
80996
+ } else {
80997
+ originalContent = await fs10.readFile(configPath, "utf-8");
80998
+ wasModified = true;
80999
+ const newConfig = `import { defineConfig } from "@playwright/test";
80789
81000
 
80790
81001
  export default defineConfig({
80791
81002
  timeout: 60000,
80792
81003
  workers: 1,
80793
81004
  fullyParallel: false,
81005
+ reporter: [
81006
+ ["list"],
81007
+ ["./timestamp-reporter.ts"],
81008
+ ],
80794
81009
  use: {
80795
81010
  video: "on",
80796
81011
  headless: true,
@@ -80799,63 +81014,18 @@ export default defineConfig({
80799
81014
  },
80800
81015
  });
80801
81016
  `;
80802
- await fs10.writeFile(configPath, defaultConfig);
80803
- console.log(`Created playwright config at ${configPath}`);
80804
- return configPath;
80805
- }
80806
- async function ensureVideoEnabled(configPath) {
80807
- const configContent = await fs10.readFile(configPath, "utf-8");
80808
- if (configContent.match(/video:\s*["']on["']/)) {
80809
- console.log("Video already enabled in config");
80810
- return null;
80811
- }
80812
- console.log("Updating config to enable video recording");
80813
- let updatedConfig = configContent;
80814
- const useBlockRegex = /use:\s*\{([^}]*)\}/s;
80815
- const match2 = configContent.match(useBlockRegex);
80816
- if (match2) {
80817
- const useBlockContent = match2[1];
80818
- if (useBlockContent.includes("video:")) {
80819
- updatedConfig = configContent.replace(
80820
- /video:\s*["'][^"']*["']/,
80821
- 'video: "on"'
80822
- );
80823
- } else {
80824
- const updatedUseBlock = `use: {
80825
- video: "on",${useBlockContent}}`;
80826
- updatedConfig = configContent.replace(useBlockRegex, updatedUseBlock);
80827
- }
80828
- } else {
80829
- const configObjectRegex = /export\s+default\s+defineConfig\(\s*\{/;
80830
- updatedConfig = configContent.replace(
80831
- configObjectRegex,
80832
- `export default defineConfig({
80833
- use: {
80834
- video: "on",
80835
- },`
81017
+ await fs10.writeFile(configPath, newConfig);
81018
+ console.log(
81019
+ `Overwrote existing config at ${configPath} (backed up for cleanup)`
80836
81020
  );
80837
81021
  }
80838
- await fs10.writeFile(configPath, updatedConfig);
80839
- console.log("Config updated to enable video recording");
80840
- return configContent;
80841
- }
80842
- async function setupPlaywrightConfig(repoLocation) {
80843
- let configPath = await findPlaywrightConfig(repoLocation);
80844
- let wasCreated = false;
80845
- let originalContent = null;
80846
- let wasModified = false;
80847
- if (!configPath) {
80848
- configPath = await createPlaywrightConfig(repoLocation);
80849
- wasCreated = true;
80850
- } else {
80851
- originalContent = await ensureVideoEnabled(configPath);
80852
- wasModified = originalContent !== null;
80853
- }
80854
81022
  return {
80855
81023
  configPath,
80856
81024
  wasCreated,
80857
81025
  originalContent,
80858
- wasModified
81026
+ wasModified,
81027
+ reporterPath,
81028
+ reporterWasCreated: true
80859
81029
  };
80860
81030
  }
80861
81031
  async function ensureTestResultsInGitignore(repoLocation) {
@@ -80928,14 +81098,48 @@ async function findVideoFile(repoLocation) {
80928
81098
  console.log(`Found video at: ${videoPath}`);
80929
81099
  return videoPath;
80930
81100
  }
80931
- async function uploadVideo(videoPath, responseUrl) {
81101
+ async function findTimestampJsonFile(repoLocation) {
81102
+ const testResultsDir = path9.join(repoLocation, "test-results");
81103
+ const walkDir = async (dir) => {
81104
+ try {
81105
+ const files = await fs10.readdir(dir);
81106
+ for (const file3 of files) {
81107
+ const fullPath = path9.join(dir, file3);
81108
+ const stat4 = await fs10.stat(fullPath);
81109
+ if (stat4.isDirectory()) {
81110
+ const result = await walkDir(fullPath);
81111
+ if (result) {
81112
+ return result;
81113
+ }
81114
+ } else if (file3.startsWith("timestamps-") && file3.endsWith(".json")) {
81115
+ return fullPath;
81116
+ }
81117
+ }
81118
+ } catch (error82) {
81119
+ console.error(`Error walking directory ${dir}:`, error82);
81120
+ }
81121
+ return null;
81122
+ };
81123
+ const jsonPath = await walkDir(testResultsDir);
81124
+ if (!jsonPath) {
81125
+ throw new Error("No timestamp JSON file found in test-results directory");
81126
+ }
81127
+ console.log(`Found timestamp JSON at: ${jsonPath}`);
81128
+ return jsonPath;
81129
+ }
81130
+ async function uploadVideo(videoPath, timestampJsonPath, responseUrl) {
80932
81131
  const videoBuffer = await fs10.readFile(videoPath);
81132
+ const jsonBuffer = await fs10.readFile(timestampJsonPath);
80933
81133
  const FormData2 = (await Promise.resolve().then(() => __toESM(require_form_data(), 1))).default;
80934
81134
  const formData = new FormData2();
80935
81135
  formData.append("video", videoBuffer, {
80936
81136
  filename: path9.basename(videoPath),
80937
81137
  contentType: "video/webm"
80938
81138
  });
81139
+ formData.append("timestamps", jsonBuffer, {
81140
+ filename: path9.basename(timestampJsonPath),
81141
+ contentType: "application/json"
81142
+ });
80939
81143
  const uploadResponse = await fetch(responseUrl, {
80940
81144
  method: "POST",
80941
81145
  body: formData,
@@ -80943,34 +81147,49 @@ async function uploadVideo(videoPath, responseUrl) {
80943
81147
  });
80944
81148
  if (!uploadResponse.ok) {
80945
81149
  throw new Error(
80946
- `Failed to upload video: ${uploadResponse.status} ${uploadResponse.statusText}`
81150
+ `Failed to upload files: ${uploadResponse.status} ${uploadResponse.statusText}`
80947
81151
  );
80948
81152
  }
80949
- console.log(`Video uploaded successfully to ${responseUrl}`);
81153
+ console.log(`Video and timestamps uploaded successfully to ${responseUrl}`);
80950
81154
  return uploadResponse.status;
80951
81155
  }
80952
- async function deleteVideo(videoPath) {
81156
+ async function deleteTestFiles(videoPath, timestampJsonPath) {
80953
81157
  try {
80954
81158
  await fs10.unlink(videoPath);
80955
81159
  console.log(`Deleted video file: ${videoPath}`);
80956
81160
  } catch (error82) {
80957
81161
  console.error(`Error deleting video file ${videoPath}:`, error82);
80958
81162
  }
81163
+ try {
81164
+ await fs10.unlink(timestampJsonPath);
81165
+ console.log(`Deleted timestamp JSON: ${timestampJsonPath}`);
81166
+ } catch (error82) {
81167
+ console.error(`Error deleting timestamp JSON ${timestampJsonPath}:`, error82);
81168
+ }
80959
81169
  }
80960
81170
  async function cleanupConfig(configState) {
80961
- if (!configState.configPath) {
80962
- return;
81171
+ if (configState.configPath) {
81172
+ try {
81173
+ if (configState.wasCreated) {
81174
+ await fs10.unlink(configState.configPath);
81175
+ console.log(
81176
+ `Cleaned up created config file: ${configState.configPath}`
81177
+ );
81178
+ } else if (configState.wasModified && configState.originalContent) {
81179
+ await fs10.writeFile(configState.configPath, configState.originalContent);
81180
+ console.log(`Restored original config: ${configState.configPath}`);
81181
+ }
81182
+ } catch (error82) {
81183
+ console.error("Error cleaning up config:", error82);
81184
+ }
80963
81185
  }
80964
- try {
80965
- if (configState.wasCreated) {
80966
- await fs10.unlink(configState.configPath);
80967
- console.log(`Cleaned up created config file: ${configState.configPath}`);
80968
- } else if (configState.wasModified && configState.originalContent) {
80969
- await fs10.writeFile(configState.configPath, configState.originalContent);
80970
- console.log(`Restored original config: ${configState.configPath}`);
81186
+ if (configState.reporterPath && configState.reporterWasCreated) {
81187
+ try {
81188
+ await fs10.unlink(configState.reporterPath);
81189
+ console.log(`Cleaned up reporter file: ${configState.reporterPath}`);
81190
+ } catch (error82) {
81191
+ console.error("Error cleaning up reporter:", error82);
80971
81192
  }
80972
- } catch (error82) {
80973
- console.error("Error cleaning up config:", error82);
80974
81193
  }
80975
81194
  }
80976
81195
  async function runPlaywrightTestWithVideo(options) {
@@ -80983,12 +81202,13 @@ async function runPlaywrightTestWithVideo(options) {
80983
81202
  await ensureTestResultsInGitignore(repoLocation);
80984
81203
  const testResult = await runPlaywrightTest(repoLocation, testFilePath);
80985
81204
  const videoPath = await findVideoFile(repoLocation);
81205
+ const timestampJsonPath = await findTimestampJsonFile(repoLocation);
80986
81206
  let uploadStatus;
80987
81207
  if (responseUrl) {
80988
- uploadStatus = await uploadVideo(videoPath, responseUrl);
80989
- await deleteVideo(videoPath);
81208
+ uploadStatus = await uploadVideo(videoPath, timestampJsonPath, responseUrl);
81209
+ await deleteTestFiles(videoPath, timestampJsonPath);
80990
81210
  } else {
80991
- console.log("No responseUrl provided, keeping video file locally");
81211
+ console.log("No responseUrl provided, keeping files locally");
80992
81212
  }
80993
81213
  await cleanupConfig(configState);
80994
81214
  return {
@@ -10905,7 +10905,7 @@ var glob = Object.assign(glob_, {
10905
10905
  glob.glob = glob;
10906
10906
 
10907
10907
  // src/proxy/version.ts
10908
- var VERSION = "0.3.24";
10908
+ var VERSION = "0.3.26";
10909
10909
 
10910
10910
  // src/cli.ts
10911
10911
  var STAKLINK_PROXY = "staklink-proxy";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "staklink",
3
3
  "displayName": "staklink",
4
4
  "description": "staklink process manager",
5
- "version": "0.3.24",
5
+ "version": "0.3.26",
6
6
  "type": "module",
7
7
  "publisher": "stakwork",
8
8
  "engines": {
@@ -0,0 +1,106 @@
1
+ {
2
+ "testTitle": "YOLO page should display YOLO text",
3
+ "testId": "ea70bc1c4bc8cb3ef066-ef50a3f3fbda7cffd2c6",
4
+ "status": "passed",
5
+ "duration": 2661,
6
+ "startTime": "2025-11-20T05:03:47.154Z",
7
+ "videoPath": "/Users/evanfeenstra/code/sphinx2/staklink/test-results/scripts-demo-test-YOLO-page-should-display-YOLO-text/video.webm",
8
+ "actions": [
9
+ {
10
+ "title": "Navigate to \"/\"",
11
+ "category": "pw:api",
12
+ "timestamp": "2025-11-20T05:03:47.791Z",
13
+ "relativeMs": 637,
14
+ "duration": 505,
15
+ "stepPath": [
16
+ "Navigate to \"/\""
17
+ ],
18
+ "location": {
19
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
20
+ "line": 5,
21
+ "column": 14
22
+ },
23
+ "sourceCode": "await page.goto('http://localhost:8080', { waitUntil: 'networkidle' });"
24
+ },
25
+ {
26
+ "title": "Wait for load state \"domcontentloaded\"",
27
+ "category": "pw:api",
28
+ "timestamp": "2025-11-20T05:03:47.793Z",
29
+ "relativeMs": 639,
30
+ "duration": 1,
31
+ "stepPath": [
32
+ "Wait for load state \"domcontentloaded\""
33
+ ],
34
+ "location": {
35
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
36
+ "line": 8,
37
+ "column": 14
38
+ },
39
+ "sourceCode": "await page.waitForLoadState('domcontentloaded');"
40
+ },
41
+ {
42
+ "title": "Expect \"toHaveText\"",
43
+ "category": "expect",
44
+ "timestamp": "2025-11-20T05:03:47.815Z",
45
+ "relativeMs": 661,
46
+ "duration": 21,
47
+ "stepPath": [
48
+ "Expect \"toHaveText\""
49
+ ],
50
+ "location": {
51
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
52
+ "line": 11,
53
+ "column": 36
54
+ },
55
+ "sourceCode": "await expect(page.locator('h1')).toHaveText('YOLO');"
56
+ },
57
+ {
58
+ "title": "Expect \"toBeVisible\"",
59
+ "category": "expect",
60
+ "timestamp": "2025-11-20T05:03:47.817Z",
61
+ "relativeMs": 663,
62
+ "duration": 2,
63
+ "stepPath": [
64
+ "Expect \"toBeVisible\""
65
+ ],
66
+ "location": {
67
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
68
+ "line": 14,
69
+ "column": 40
70
+ },
71
+ "sourceCode": "await expect(page.locator('button')).toBeVisible();"
72
+ },
73
+ {
74
+ "title": "Expect \"toHaveText\"",
75
+ "category": "expect",
76
+ "timestamp": "2025-11-20T05:03:47.819Z",
77
+ "relativeMs": 665,
78
+ "duration": 2,
79
+ "stepPath": [
80
+ "Expect \"toHaveText\""
81
+ ],
82
+ "location": {
83
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
84
+ "line": 15,
85
+ "column": 40
86
+ },
87
+ "sourceCode": "await expect(page.locator('button')).toHaveText('Test');"
88
+ },
89
+ {
90
+ "title": "Wait for timeout",
91
+ "category": "pw:api",
92
+ "timestamp": "2025-11-20T05:03:49.821Z",
93
+ "relativeMs": 2667,
94
+ "duration": 2002,
95
+ "stepPath": [
96
+ "Wait for timeout"
97
+ ],
98
+ "location": {
99
+ "file": "/Users/evanfeenstra/code/sphinx2/staklink/scripts/demo-test.spec.ts",
100
+ "line": 18,
101
+ "column": 14
102
+ },
103
+ "sourceCode": "await page.waitForTimeout(2000);"
104
+ }
105
+ ]
106
+ }