Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1896bdf9 | |||
| fdc84a2d83 | |||
| d92850e1d2 | |||
| 6c498b3686 | |||
| 913c3cafe8 | |||
| 9ec2c8b6eb | |||
| 286030a08d | |||
| 46f0a5a8cf | |||
| ae59b7adf2 | |||
| 2b81e8e5aa |
1
.serena/.gitignore
vendored
1
.serena/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/cache
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
|
||||||
# * For C, use cpp
|
|
||||||
# * For JavaScript, use typescript
|
|
||||||
# Special requirements:
|
|
||||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
|
||||||
language: typescript
|
|
||||||
|
|
||||||
# whether to use the project's gitignore file to ignore files
|
|
||||||
# Added on 2025-04-07
|
|
||||||
ignore_all_files_in_gitignore: true
|
|
||||||
# list of additional paths to ignore
|
|
||||||
# same syntax as gitignore, so you can use * and **
|
|
||||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
|
||||||
# Added (renamed) on 2025-04-07
|
|
||||||
ignored_paths: []
|
|
||||||
|
|
||||||
# whether the project is in read-only mode
|
|
||||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
|
||||||
# Added on 2025-04-18
|
|
||||||
read_only: false
|
|
||||||
|
|
||||||
|
|
||||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
|
||||||
# Below is the complete list of tools for convenience.
|
|
||||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
|
||||||
# execute `uv run scripts/print_tool_overview.py`.
|
|
||||||
#
|
|
||||||
# * `activate_project`: Activates a project by name.
|
|
||||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
|
||||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
|
||||||
# * `delete_lines`: Deletes a range of lines within a file.
|
|
||||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
|
||||||
# * `execute_shell_command`: Executes a shell command.
|
|
||||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
|
||||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
|
||||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
|
||||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
|
||||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
|
||||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
|
||||||
# Should only be used in settings where the system prompt cannot be set,
|
|
||||||
# e.g. in clients you have no control over, like Claude Desktop.
|
|
||||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
|
||||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
|
||||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
|
||||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
|
||||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
|
||||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
|
||||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
|
||||||
# * `read_file`: Reads a file within the project directory.
|
|
||||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
|
||||||
# * `remove_project`: Removes a project from the Serena configuration.
|
|
||||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
|
||||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
|
||||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
|
||||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
|
||||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
|
||||||
# * `switch_modes`: Activates modes by providing a list of their names
|
|
||||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
|
||||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
|
||||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
|
||||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
|
||||||
excluded_tools: []
|
|
||||||
|
|
||||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
|
||||||
# (contrary to the memories, which are loaded on demand).
|
|
||||||
initial_prompt: ""
|
|
||||||
|
|
||||||
project_name: "tstest"
|
|
||||||
37
changelog.md
37
changelog.md
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-01-25 - 3.1.8 - fix(tapbundle)
|
||||||
|
treat tests that call tools.allowFailure() as passing and update tests to use tools parameter
|
||||||
|
|
||||||
|
- Set testResult.ok to this.failureAllowed so allowed failures are considered passing in the tap test runner implementation (ts_tapbundle/tapbundle.classes.taptest.ts).
|
||||||
|
- Updated multiple tests to accept the tools parameter and call tools.allowFailure() where failures are intended (test/tapbundle/test.performance-metrics.ts, test/tstest/test.fail.ts, test/tstest/test.failing-with-logs.ts).
|
||||||
|
- Prevents intentionally-failing tests from skewing timing/metric calculations and preserves console logs for allowed failures.
|
||||||
|
|
||||||
|
## 2026-01-25 - 3.1.7 - fix(tap-parser)
|
||||||
|
append newline to WebSocket tap log messages to ensure proper line-by-line processing
|
||||||
|
|
||||||
|
- Fixes handling of WebSocket console.log messages by appending a trailing newline before processing to avoid merged lines.
|
||||||
|
- Modified ts/tstest.classes.tap.parser.ts: handleTapLog now calls _processLog(tapLog + '\n').
|
||||||
|
|
||||||
|
## 2026-01-19 - 3.1.6 - fix(logging)
|
||||||
|
handle mid-line streaming output in test logger and add streaming tests
|
||||||
|
|
||||||
|
- Introduce isOutputMidLine flag to track when streaming output does not end with a newline
|
||||||
|
- Only prepend the visual prefix at the start of a line and append segments to the last buffered entry when mid-line
|
||||||
|
- Write consistent output to log files for both complete lines and raw streaming segments
|
||||||
|
- Add tests to exercise streaming behavior: test/tstest/test.gap-debug.ts and test/tstest/test.gap-debug2.ts
|
||||||
|
|
||||||
|
## 2026-01-19 - 3.1.5 - fix(tstest)
|
||||||
|
preserve streaming console output and correctly buffer incomplete TAP lines
|
||||||
|
|
||||||
|
- Reworked TapParser._processLog to buffer incomplete lines and only parse complete TAP protocol lines
|
||||||
|
- Added TapParser.lineBuffer and _looksLikeTapStart() to detect and buffer starts of TAP messages
|
||||||
|
- Added TapParser._handleConsoleOutput() to centralize console output handling and snapshot parsing; flushes buffered content on process exit
|
||||||
|
- Added TapTestResult.addLogLineRaw() to append streaming text without adding newlines
|
||||||
|
- Added TsTestLogger.testConsoleOutputStreaming() and logToTestFileRaw() to preserve streaming output formatting in both console and logfile
|
||||||
|
|
||||||
|
## 2025-12-30 - 3.1.4 - fix(webhelpers)
|
||||||
|
improve browser test fixture to append element and await custom element upgrade and Lit update completion; add generic return type; update npm packaging release config; remove pnpm onlyBuiltDependencies
|
||||||
|
|
||||||
|
- ts_tapbundle/webhelpers.ts: make fixture generic and return T; append created element to document; await customElements.whenDefined for custom elements and await updateComplete for Lit/async components to ensure stable rendering in tests
|
||||||
|
- npmextra.json: add @git.zone/cli module metadata and release.registries/accessLevel; add @ship.zone/szci entry
|
||||||
|
- pnpm-workspace.yaml: remove onlyBuiltDependencies entries
|
||||||
|
|
||||||
## 2025-11-21 - 3.1.3 - fix(docs)
|
## 2025-11-21 - 3.1.3 - fix(docs)
|
||||||
Update package author and expand license/legal and issue-reporting information in tapbundle docs
|
Update package author and expand license/legal and issue-reporting information in tapbundle docs
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"target": "ES2022"
|
"target": "ES2022"
|
||||||
},
|
},
|
||||||
"nodeModulesDir": true,
|
"nodeModulesDir": true,
|
||||||
"version": "3.1.3"
|
"version": "3.1.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"npmci": {
|
"@git.zone/cli": {
|
||||||
"npmGlobalTools": [],
|
|
||||||
"npmAccessLevel": "public"
|
|
||||||
},
|
|
||||||
"gitzone": {
|
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
@@ -12,6 +8,16 @@
|
|||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"npmPackagename": "@git.zone/tstest",
|
"npmPackagename": "@git.zone/tstest",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {
|
||||||
|
"npmGlobalTools": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "3.1.3",
|
"version": "3.1.8",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
onlyBuiltDependencies:
|
|
||||||
- esbuild
|
|
||||||
- mongodb-memory-server
|
|
||||||
- puppeteer
|
|
||||||
@@ -445,4 +445,49 @@ The protocol parser was fixed to correctly handle inline timing metadata:
|
|||||||
- Changed condition from `!simpleMatch[1].includes(':')` to check for simple key:value pairs
|
- Changed condition from `!simpleMatch[1].includes(':')` to check for simple key:value pairs
|
||||||
- Excludes prefixed formats (META:, SKIP:, TODO:, EVENT:) while parsing simple formats like `time:250`
|
- Excludes prefixed formats (META:, SKIP:, TODO:, EVENT:) while parsing simple formats like `time:250`
|
||||||
|
|
||||||
This ensures timing metadata is correctly extracted and displayed in test results.
|
This ensures timing metadata is correctly extracted and displayed in test results.
|
||||||
|
|
||||||
|
## Streaming Console Output (Fixed)
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
When tests use `process.stdout.write()` for streaming output (without newlines), each write was appearing on a separate line. This happened because:
|
||||||
|
1. Child process stdout data events arrive as separate chunks
|
||||||
|
2. `TapParser._processLog()` split on `\n` and processed each segment
|
||||||
|
3. `testConsoleOutput()` used `console.log()` which added a newline to each call
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
The streaming behavior is now preserved by:
|
||||||
|
1. **Line buffering for TAP parsing**: Only buffer content that looks like TAP protocol messages
|
||||||
|
2. **True streaming for console output**: Use `process.stdout.write()` instead of `console.log()` for partial lines
|
||||||
|
3. **Intelligent detection**: `_looksLikeTapStart()` checks if content could be a TAP protocol message
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**TapParser changes:**
|
||||||
|
- Added `lineBuffer` property to buffer incomplete TAP protocol lines
|
||||||
|
- Rewrote `_processLog()` to handle streaming correctly:
|
||||||
|
- Complete lines (with newline) are processed through protocol parser
|
||||||
|
- Incomplete lines that look like TAP are buffered
|
||||||
|
- Incomplete lines that don't look like TAP are streamed immediately
|
||||||
|
- Added `_looksLikeTapStart()` helper to detect TAP protocol patterns
|
||||||
|
- Added `_handleConsoleOutput()` to handle console output with proper streaming
|
||||||
|
- Buffer is flushed on process exit
|
||||||
|
|
||||||
|
**TsTestLogger changes:**
|
||||||
|
- Added `testConsoleOutputStreaming()` method that uses `process.stdout.write()` in verbose mode
|
||||||
|
- Added `logToTestFileRaw()` for writing to log files without adding newlines
|
||||||
|
- In non-verbose mode, streaming content is appended to the last buffered entry
|
||||||
|
|
||||||
|
**TapTestResult changes:**
|
||||||
|
- Added `addLogLineRaw()` method that doesn't append newlines
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Tests can now use streaming output naturally:
|
||||||
|
```typescript
|
||||||
|
process.stdout.write("Loading");
|
||||||
|
process.stdout.write(".");
|
||||||
|
process.stdout.write(".");
|
||||||
|
process.stdout.write(".\n");
|
||||||
|
```
|
||||||
|
|
||||||
|
This will correctly display as `Loading...` on a single line in verbose mode.
|
||||||
@@ -47,6 +47,7 @@ tap.test('metric test fast 3 - minimal work', async () => {
|
|||||||
|
|
||||||
// Test to verify that failed tests still contribute to timing metrics
|
// Test to verify that failed tests still contribute to timing metrics
|
||||||
tap.test('metric test that fails - 60ms before failure', async (tools) => {
|
tap.test('metric test that fails - 60ms before failure', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
await tools.delayFor(60);
|
await tools.delayFor(60);
|
||||||
expect(true).toBeFalse(); // This will fail
|
expect(true).toBeFalse(); // This will fail
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
tap.test('This test should fail', async () => {
|
tap.test('This test should fail', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
console.log('This test will fail on purpose');
|
console.log('This test will fail on purpose');
|
||||||
expect(true).toBeFalse();
|
expect(true).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { expect, tap } from '../../ts_tapbundle/index.js';
|
import { expect, tap } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
tap.test('Test that will fail with console logs', async () => {
|
tap.test('Test that will fail with console logs', async (tools) => {
|
||||||
|
tools.allowFailure();
|
||||||
console.log('Starting the test...');
|
console.log('Starting the test...');
|
||||||
console.log('Doing some setup work');
|
console.log('Doing some setup work');
|
||||||
console.log('About to check assertion');
|
console.log('About to check assertion');
|
||||||
|
|
||||||
const value = 42;
|
const value = 42;
|
||||||
console.log(`The value is: ${value}`);
|
console.log(`The value is: ${value}`);
|
||||||
|
|
||||||
// This will fail
|
// This will fail
|
||||||
expect(value).toEqual(100);
|
expect(value).toEqual(100);
|
||||||
|
|
||||||
console.log('This log will not be reached');
|
console.log('This log will not be reached');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
test/tstest/test.gap-debug.ts
Normal file
12
test/tstest/test.gap-debug.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('check for gaps in streaming', async () => {
|
||||||
|
// This should print "ABCD" with no gaps
|
||||||
|
process.stdout.write("A");
|
||||||
|
process.stdout.write("B");
|
||||||
|
process.stdout.write("C");
|
||||||
|
process.stdout.write("D\n");
|
||||||
|
expect(true).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
14
test/tstest/test.gap-debug2.ts
Normal file
14
test/tstest/test.gap-debug2.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('streaming with delays', async (tools) => {
|
||||||
|
// Simulate real streaming with delays
|
||||||
|
process.stdout.write("Progress: [");
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await tools.delayFor(50);
|
||||||
|
process.stdout.write("=");
|
||||||
|
}
|
||||||
|
process.stdout.write("]\n");
|
||||||
|
expect(true).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '3.1.3',
|
version: '3.1.8',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ export class TapParser {
|
|||||||
receivedTests: number = 0;
|
receivedTests: number = 0;
|
||||||
|
|
||||||
activeTapTestResult: TapTestResult;
|
activeTapTestResult: TapTestResult;
|
||||||
|
|
||||||
private logger: TsTestLogger;
|
private logger: TsTestLogger;
|
||||||
private protocolParser: ProtocolParser;
|
private protocolParser: ProtocolParser;
|
||||||
private protocolVersion: string | null = null;
|
private protocolVersion: string | null = null;
|
||||||
private startTime: number;
|
private startTime: number;
|
||||||
|
private lineBuffer: string = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the constructor for TapParser
|
* the constructor for TapParser
|
||||||
@@ -71,42 +72,99 @@ export class TapParser {
|
|||||||
if (Buffer.isBuffer(logChunk)) {
|
if (Buffer.isBuffer(logChunk)) {
|
||||||
logChunk = logChunk.toString();
|
logChunk = logChunk.toString();
|
||||||
}
|
}
|
||||||
const logLineArray = logChunk.split('\n');
|
|
||||||
if (logLineArray[logLineArray.length - 1] === '') {
|
// Prepend any buffered content from previous incomplete line
|
||||||
logLineArray.pop();
|
const fullChunk = this.lineBuffer + logChunk;
|
||||||
|
this.lineBuffer = '';
|
||||||
|
|
||||||
|
// Split into segments by newline
|
||||||
|
const segments = fullChunk.split('\n');
|
||||||
|
const lastIndex = segments.length - 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const segment = segments[i];
|
||||||
|
const isLastSegment = (i === lastIndex);
|
||||||
|
const hasNewline = !isLastSegment; // All segments except last had a newline after them
|
||||||
|
|
||||||
|
if (hasNewline) {
|
||||||
|
// Complete line - check if it's a TAP protocol message
|
||||||
|
const messages = this.protocolParser.parseLine(segment);
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
// Handle protocol messages
|
||||||
|
for (const message of messages) {
|
||||||
|
this._handleProtocolMessage(message, segment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-protocol complete line - handle as console output
|
||||||
|
this._handleConsoleOutput(segment, true);
|
||||||
|
}
|
||||||
|
} else if (segment) {
|
||||||
|
// Last segment without newline - could be:
|
||||||
|
// 1. Partial console output (stream immediately)
|
||||||
|
// 2. Start of a TAP message (need to buffer for protocol parsing)
|
||||||
|
|
||||||
|
// Check if it looks like the start of a TAP protocol message
|
||||||
|
if (this._looksLikeTapStart(segment)) {
|
||||||
|
// Buffer it for complete line parsing
|
||||||
|
this.lineBuffer = segment;
|
||||||
|
} else {
|
||||||
|
// Stream immediately as console output (no newline)
|
||||||
|
this._handleConsoleOutput(segment, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if text could be the start of a TAP protocol message
|
||||||
|
*/
|
||||||
|
private _looksLikeTapStart(text: string): boolean {
|
||||||
|
return (
|
||||||
|
text.startsWith('ok ') ||
|
||||||
|
text.startsWith('not ok ') ||
|
||||||
|
text.startsWith('1..') ||
|
||||||
|
text.startsWith('# ') ||
|
||||||
|
text.startsWith('TAP version ') ||
|
||||||
|
text.startsWith('⟦TSTEST:') ||
|
||||||
|
text.startsWith('Bail out!')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle console output from test, preserving streaming behavior
|
||||||
|
*/
|
||||||
|
private _handleConsoleOutput(text: string, hasNewline: boolean) {
|
||||||
|
// Check for snapshot communication (legacy)
|
||||||
|
const snapshotMatch = text.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
|
||||||
|
if (snapshotMatch) {
|
||||||
|
const base64Data = snapshotMatch[1];
|
||||||
|
try {
|
||||||
|
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
|
||||||
|
this.handleSnapshot(snapshotData);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (this.logger) {
|
||||||
|
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each line through the protocol parser
|
// Add to test result buffer
|
||||||
for (const logLine of logLineArray) {
|
if (this.activeTapTestResult) {
|
||||||
const messages = this.protocolParser.parseLine(logLine);
|
if (hasNewline) {
|
||||||
|
this.activeTapTestResult.addLogLine(text);
|
||||||
if (messages.length > 0) {
|
|
||||||
// Handle protocol messages
|
|
||||||
for (const message of messages) {
|
|
||||||
this._handleProtocolMessage(message, logLine);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Not a protocol message, handle as console output
|
this.activeTapTestResult.addLogLineRaw(text);
|
||||||
if (this.activeTapTestResult) {
|
}
|
||||||
this.activeTapTestResult.addLogLine(logLine);
|
}
|
||||||
}
|
|
||||||
|
// Output to logger with streaming support
|
||||||
// Check for snapshot communication (legacy)
|
if (this.logger) {
|
||||||
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
|
if (hasNewline) {
|
||||||
if (snapshotMatch) {
|
this.logger.testConsoleOutput(text);
|
||||||
const base64Data = snapshotMatch[1];
|
} else {
|
||||||
try {
|
this.logger.testConsoleOutputStreaming(text);
|
||||||
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
|
|
||||||
this.handleSnapshot(snapshotData);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (this.logger) {
|
|
||||||
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.logger) {
|
|
||||||
// This is console output from the test file
|
|
||||||
this.logger.testConsoleOutput(logLine);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,6 +475,11 @@ export class TapParser {
|
|||||||
this._processLog(data);
|
this._processLog(data);
|
||||||
});
|
});
|
||||||
childProcessArg.on('exit', async () => {
|
childProcessArg.on('exit', async () => {
|
||||||
|
// Flush any remaining buffered content
|
||||||
|
if (this.lineBuffer) {
|
||||||
|
this._handleConsoleOutput(this.lineBuffer, false);
|
||||||
|
this.lineBuffer = '';
|
||||||
|
}
|
||||||
await this.evaluateFinalResult();
|
await this.evaluateFinalResult();
|
||||||
done.resolve();
|
done.resolve();
|
||||||
});
|
});
|
||||||
@@ -424,7 +487,9 @@ export class TapParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleTapLog(tapLog: string) {
|
public async handleTapLog(tapLog: string) {
|
||||||
this._processLog(tapLog);
|
// Each WebSocket message represents a complete console.log() call,
|
||||||
|
// so append newline to ensure proper line-by-line processing
|
||||||
|
this._processLog(tapLog + '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class TapTestResult {
|
|||||||
constructor(public id: number) {}
|
constructor(public id: number) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* adds a logLine to the log buffer of the test
|
* adds a logLine to the log buffer of the test (with newline appended)
|
||||||
* @param logLine
|
* @param logLine
|
||||||
*/
|
*/
|
||||||
addLogLine(logLine: string) {
|
addLogLine(logLine: string) {
|
||||||
@@ -19,6 +19,15 @@ export class TapTestResult {
|
|||||||
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
|
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adds raw text to the log buffer without appending newline (for streaming output)
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
addLogLineRaw(text: string) {
|
||||||
|
const logLineBuffer = Buffer.from(text);
|
||||||
|
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
|
||||||
|
}
|
||||||
|
|
||||||
setTestResult(testOkArg: boolean) {
|
setTestResult(testOkArg: boolean) {
|
||||||
this.testOk = testOkArg;
|
this.testOk = testOkArg;
|
||||||
this.testSettled = true;
|
this.testSettled = true;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export class TsTestLogger {
|
|||||||
private currentTestLogFile: string | null = null;
|
private currentTestLogFile: string | null = null;
|
||||||
private currentTestLogs: string[] = []; // Buffer for current test logs
|
private currentTestLogs: string[] = []; // Buffer for current test logs
|
||||||
private currentTestFailed: boolean = false;
|
private currentTestFailed: boolean = false;
|
||||||
|
private isOutputMidLine: boolean = false; // Track whether we're mid-line for streaming output
|
||||||
|
|
||||||
constructor(options: LogOptions = {}) {
|
constructor(options: LogOptions = {}) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@@ -189,6 +190,7 @@ export class TsTestLogger {
|
|||||||
// Reset test-specific state
|
// Reset test-specific state
|
||||||
this.currentTestLogs = [];
|
this.currentTestLogs = [];
|
||||||
this.currentTestFailed = false;
|
this.currentTestFailed = false;
|
||||||
|
this.isOutputMidLine = false;
|
||||||
|
|
||||||
// Only set up test log file if --logfile option is specified
|
// Only set up test log file if --logfile option is specified
|
||||||
if (this.options.logFile) {
|
if (this.options.logFile) {
|
||||||
@@ -348,21 +350,73 @@ export class TsTestLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Console output from test files (non-TAP output)
|
// Console output from test files (non-TAP output) - complete lines
|
||||||
testConsoleOutput(message: string) {
|
testConsoleOutput(message: string) {
|
||||||
if (this.options.json) return;
|
if (this.options.json) return;
|
||||||
|
|
||||||
|
const prefix = ' ';
|
||||||
// In verbose mode, show console output immediately
|
// In verbose mode, show console output immediately
|
||||||
if (this.options.verbose) {
|
if (this.options.verbose) {
|
||||||
this.log(this.format(` ${message}`, 'dim'));
|
// Only add prefix if we're starting a new line
|
||||||
|
const output = this.isOutputMidLine ? message : prefix + message;
|
||||||
|
this.log(this.format(output, 'dim'));
|
||||||
} else {
|
} else {
|
||||||
// In non-verbose mode, buffer the logs
|
// In non-verbose mode, buffer the logs
|
||||||
this.currentTestLogs.push(message);
|
if (this.isOutputMidLine && this.currentTestLogs.length > 0) {
|
||||||
|
// Append to the last buffered entry since we're mid-line
|
||||||
|
this.currentTestLogs[this.currentTestLogs.length - 1] += message;
|
||||||
|
} else {
|
||||||
|
this.currentTestLogs.push(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset mid-line state since we just output a complete line
|
||||||
|
this.isOutputMidLine = false;
|
||||||
|
|
||||||
// Always log to test file if --logfile is specified
|
// Always log to test file if --logfile is specified
|
||||||
if (this.currentTestLogFile) {
|
if (this.currentTestLogFile) {
|
||||||
this.logToTestFile(` ${message}`);
|
this.logToTestFile(`${prefix}${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming console output (preserves original formatting, no newline added)
|
||||||
|
testConsoleOutputStreaming(message: string) {
|
||||||
|
if (this.options.json) return;
|
||||||
|
|
||||||
|
const prefix = ' ';
|
||||||
|
// Only add prefix if we're starting a new line (not mid-line)
|
||||||
|
const output = this.isOutputMidLine ? message : prefix + message;
|
||||||
|
|
||||||
|
if (this.options.verbose) {
|
||||||
|
// Use process.stdout.write to preserve streaming without adding newlines
|
||||||
|
process.stdout.write(this.format(output, 'dim'));
|
||||||
|
} else {
|
||||||
|
// Buffer mode: append to last entry if mid-line
|
||||||
|
if (this.isOutputMidLine && this.currentTestLogs.length > 0) {
|
||||||
|
this.currentTestLogs[this.currentTestLogs.length - 1] += message;
|
||||||
|
} else {
|
||||||
|
this.currentTestLogs.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log to test file without adding newline
|
||||||
|
if (this.currentTestLogFile) {
|
||||||
|
this.logToTestFileRaw(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're now mid-line (no newline was written)
|
||||||
|
this.isOutputMidLine = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private logToTestFileRaw(message: string) {
|
||||||
|
try {
|
||||||
|
// Remove ANSI color codes for file logging
|
||||||
|
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
|
||||||
|
|
||||||
|
// Append to test log file without adding newline
|
||||||
|
fs.appendFileSync(this.currentTestLogFile, cleanMessage);
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail to avoid disrupting the test run
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export class TapTest<T = unknown> {
|
|||||||
|
|
||||||
// Final failure
|
// Final failure
|
||||||
const testResult = {
|
const testResult = {
|
||||||
ok: false,
|
ok: this.failureAllowed, // Pass if failure is allowed
|
||||||
testNumber,
|
testNumber,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -22,10 +22,24 @@ class WebHelpers {
|
|||||||
|
|
||||||
// Initialize fixture function based on environment
|
// Initialize fixture function based on environment
|
||||||
if (smartenv.isBrowser) {
|
if (smartenv.isBrowser) {
|
||||||
this.fixture = async (htmlString: string): Promise<HTMLElement> => {
|
this.fixture = async <T extends HTMLElement>(htmlString: string): Promise<T> => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.innerHTML = htmlString.trim();
|
container.innerHTML = htmlString.trim();
|
||||||
const element = container.firstChild as HTMLElement;
|
const element = container.firstElementChild as T;
|
||||||
|
|
||||||
|
// Append to document so custom elements upgrade and lifecycle hooks fire
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
// Wait for custom element definition if it's a custom element
|
||||||
|
if (element.localName.includes('-')) {
|
||||||
|
await customElements.whenDefined(element.localName).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Lit/async components to finish rendering
|
||||||
|
if ((element as any).updateComplete) {
|
||||||
|
await (element as any).updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user