Skip to content

Feat watch mode - #804

Open
sonic16x wants to merge 3 commits into
masterfrom
watch-mode
Open

Feat watch mode#804
sonic16x wants to merge 3 commits into
masterfrom
watch-mode

Conversation

@sonic16x

@sonic16x sonic16x commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Watch mode for gui mode.

Usage:
npx testplane gui --watch

Screen.Recording.2026-09-03.at.01.35.28.mov

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/html-reporter@804

commit: dff7146

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

✅ Component tests succeed

Report

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

✅ E2E tests succeed

Report

@sonic16x
sonic16x force-pushed the watch-mode branch 3 times, most recently from 62eb636 to 54369aa Compare September 7, 2026 07:38

@KuznetsovRoman KuznetsovRoman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate more modular structure, where tests watcher would be pulled to separate module with short and concise module calls instead of being splattered all over the code base.

Comment thread lib/gui/server.ts

await app.initialize();

if (args.cli.options.watch && toolAdapter.toolName === ToolName.Testplane) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not the clean architecture i would like to see in html-reporter.
We have a lot of layers in html-reporter (server launches app, which manages tool-runner) to distribute responsibility between the layers, and you just pasted entire watcher in "gui/server.ts".

Now "server.ts" handles chokidar, calculates scope, manages debounce, drain the que, manages this "relatively low" level error handling...

Its clearly should be another module, and not "server.ts" responsibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Moved watcher logic to separate TestsWatcher module. Now server.ts only creates and starts it.

Comment thread lib/gui/server.ts Outdated
Comment on lines +316 to +317
const watchPaths = [...new Set([...config.getTestFilePatterns(), ...args.paths])];
const watchRoots = [...new Set(watchPaths.map(getWatchRoot).filter(Boolean))];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are building watch roots, that may differ from real testplane scope:

  • If we have empty default set with files: [], your code gets empty array, while testplane reads default test directories
  • When running npx testplane gui tests/a.ts --watch, watches subscribes on all of the config globs, which is not what should be done here.
  • Selecting set through CLI call also is not considered
  • If path is set like "tests" without ending "/" or asterisks on the end, your "path.dirname('tests')" returns ".", so the watcher looks at all project files, including node_modules

Examples:
getWatchRoot('tests') -> .
getWatchRoot('tests/') -> tests
getWatchRoot('tests/**/*.ts') -> tests

As a solution i see "creating watch plan in testplane adapter the same way test set is built for initial read". So it would be single source of truth, and not "couple of testfile finding mechanisms, which can work differently"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Watch plan is now created in Testplane adapter. It handles CLI paths, selected sets, environment sets and default folders. Added tests for this cases.

Comment thread lib/gui/server.ts Outdated
let refreshSequence = 0;
let firstDebouncedEventAt: number | undefined;

const refresh = async (changedFiles: string[], removedDirectories: string[]): Promise<void> => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a "catch" (thats a wordplay) in this function:
If some step, like, retrieving sqlite from history, fails, browser would only get "TESTS_REFRESH_FAILED" without actual tree patch. So if there was test A, which was renamed to test B, after the error browser would see test A, not test B, despite server collection would have the opposite: test B, not test A.

Trying to run test A would likely result in silent "nothing is really launched"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Collection is updated only after successful tree update. If update fails, previous tree, test adapters and attempts are restored. Added test for this case.

Comment thread lib/gui/tool-runner/index.ts Outdated
}
}
const isChangedFile = (test: TestAdapter): boolean => affectedFiles.has(path.resolve(test.file));
const signature = (test: TestAdapter): string => JSON.stringify([test.browserId, path.resolve(test.file), test.titlePath]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your signature only includes "browserId", "resolved file" and "titlePath"
But tree building also consider "disabled", "silentlySkipped" and "pending"
For example, changing "it" to "it.skip" would have the exact signature and therefore would not refresh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if (filesToRead.length) {
try {
stageStartedAt = performance.now();
changedCollection = await this._toolAdapter.readTests(filesToRead, this._globalOpts);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you separately check for "unique full name" here?
"readTests" under the hood checks for unique full names only for tests it currently read.
But you read only subset of tests and then combine it with previous tree without checking for fullName duplicates. It could potentially lead into some hard-to-debug problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Partial update now checks duplicate test names in changed and unchanged files. Added test for this case.

Comment thread lib/static/modules/search/worker.ts Outdated
items: Object.keys(event.data.data).length
});
}
self.postMessage(true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now worker sends "true" on both "init" and "patch"

Existing "search" function in this file expects array of strings

If we get "true" from worker after search was initiated, "search" could confuse its response as "response to search" and fail due to "TypeError: array expected" or something like that.

I think we should move to typed messages: dispatching not just single "true", but whole object with "type" field in it in order to avoid confusion.

Comment thread lib/static/modules/search/index.ts Outdated
}
});

worker.postMessage({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked that "Test DOES appear when there was a filter, preventing from showing it, because its old name did not suite the filter, but the new one - does"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Worker messages now have type and requestId, so different responses are not mixed. Added test for this case.

Comment thread lib/gui/server.ts Outdated
}
};

testsWatcher = chokidar.watch(watchPaths, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, "chokidar.watch" errors crash the whole gui process. Is it expected? Is it like super stable so we wont expect it ever happening?

I would add some kind of "catch", at least for easier debugging (when somebody would see scary error message from chokidar.watch exception, he should at least understand that the error was because of some king of error in tree watcher)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Current search filter runs again after search index update. Added test for renamed test case.

Comment thread lib/sqlite-client.ts Outdated
Comment on lines +185 to +188
for (const {suitePath, browserId} of uniqueTests) {
const statement = this._db.prepare(
`SELECT * FROM ${DB_SUITES_TABLE_NAME} WHERE suitePath = ? AND name = ?`
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you doing N full scans? Its not clickhouse with its magic data skipping indexes. Its better to iterate through every "suite" once and filter everything you need by yourself, instead of torturing SQLite like that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

cwd: process.cwd(),
ignoreInitial: true,
ignored: [
/(^|[/\\])\../,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it would ignore .tests/case.ts

We actually have some concrete examples: https://nda.ya.ru/t/VVkNi_VO7qaK7o

Comment on lines +82 to +89
const getTestStructureSignature = (test: TestAdapter): string => JSON.stringify([
test.browserId,
path.resolve(test.file),
test.titlePath,
test.disabled,
test.silentlySkipped,
test.pending
]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also add skip reason here?
Now skip reason update would not update real test in the tree because its not a part of the key

Comment on lines +155 to +157
logger.log(`[watch-perf][server][#${refreshId}] serialize/write SSE: ${(performance.now() - sendStartedAt).toFixed(1)}ms`);
}
logger.log(`[watch-perf][server][#${refreshId}] refresh loop total: ${(performance.now() - refreshStartedAt).toFixed(1)}ms ${JSON.stringify({changed})}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those type of logs would be removed before merging, right?
If you want to leave them in the code, you should at least hide them in "debug"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I will remove it, it is just for review and tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants