Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to `@qavajs/tx` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Changed
- Command log entries no longer have a coloured left border — status is conveyed by the icon only
- Inline test log now collapses automatically when a test finishes, regardless of pass or fail

### Removed
- `:has-text("…")` pseudo-class support in selectors — use `locator.filter({ hasText: '…' })` instead

## [0.0.13]

### Changed
- File extension restriction removed from test source requests — spec files with non-`.js`/`.ts` extensions are now accepted by the server

## [0.0.12]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ console.log(resp.status()); // 200
```ts
page.locator(selector: string): Locator
```
Match elements by CSS selector. Supports the `:has-text("…")` pseudo-class.
Match elements by CSS selector.

```ts
page.getByText(text: string | RegExp, opts?: { exact?: boolean }): Locator
Expand Down
5 changes: 2 additions & 3 deletions src/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { WindowConfig } from '../types';
import { Route, routeHandlers as _routeHandlers, matchesRoutePattern as _matchesRoutePattern } from './route';
export { Route };
import { installEventBridges as _installEventBridges, installWindowBridges as _installWindowBridges } from './bridges';
import { Locator, textMatches, resolveSelector, _locatorHandlers } from './locator';
import { Locator, resolveSelector, _locatorHandlers } from './locator';
import { makeLocatorQueries } from './locator-queries';
import { ariaSnapshot as _ariaSnapshot } from './aria';
import { Mouse } from './mouse';
Expand Down Expand Up @@ -616,9 +616,8 @@ export const page = {
const parts = resolveSelector(selector);
const seen = new Set<Element>();
const out: Element[] = [];
for (const { base, hasText } of parts) {
for (const base of parts) {
for (const el of Array.from(doc.querySelectorAll(base))) {
if (hasText && !textMatches(el, hasText)) continue;
if (!seen.has(el)) { seen.add(el); out.push(el); }
}
}
Expand Down
12 changes: 2 additions & 10 deletions src/browser/locator-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@ export function textMatches(el: Element, text: string | RegExp, exact = false):
return text instanceof RegExp ? text.test(t) : exact ? t === text : t.includes(text);
}

export function resolveSelector(selector: string): { base: string; hasText: string | null }[] {
return selector.split(',').map(s => {
s = s.trim();
const m = s.match(/:has-text\(["'](.+?)["']\)/);
if (m) {
const base = s.replace(/:has-text\(["'](.+?)["']\)/, '').trim() || '*';
return { base, hasText: m[1] };
}
return { base: s, hasText: null };
});
export function resolveSelector(selector: string): string[] {
return selector.split(',').map(s => s.trim());
}
6 changes: 2 additions & 4 deletions src/browser/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,8 @@ export class Locator {
const seen = new Set<Element>();
const out: Element[] = [];
for (const root of this._els()) {
for (const { base, hasText } of parts) {
for (const base of parts) {
for (const el of Array.from(root.querySelectorAll(base))) {
if (hasText && !textMatches(el, hasText)) continue;
if (!seen.has(el)) { seen.add(el); out.push(el); }
}
}
Expand Down Expand Up @@ -526,9 +525,8 @@ export class FrameLocator {
const parts = resolveSelector(selector);
const seen = new Set<Element>();
const out: Element[] = [];
for (const { base, hasText } of parts) {
for (const base of parts) {
for (const el of Array.from(doc.querySelectorAll(base))) {
if (hasText && !textMatches(el, hasText)) continue;
if (!seen.has(el)) { seen.add(el); out.push(el); }
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/core/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,9 @@ async function fetchAndRun(
activateTestLog(opts.filename!, testName, attempt);
} : undefined,
onAttemptError: opts?.filename ? appendErrorToLog : undefined,
onAttemptFinally: opts?.filename ? (testName, passed, attemptsLeft) => {
onAttemptFinally: opts?.filename ? (testName) => {
const logEl = document.getElementById('tlog-' + escAttr(opts.filename! + '\x01' + testName));
if (!passed || attemptsLeft > 0) logEl?.classList.remove('open');
logEl?.classList.remove('open');
_activeTestLog = null;
} : undefined,
onTestEnd: (r) => {
Expand Down
7 changes: 0 additions & 7 deletions src/panel/controlPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,16 +560,9 @@ export function generateControlPanelHTML({ proxyUrl, controlPanelPort = DEFAULT_
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.55;
border-left: 2px solid transparent;
}
.tx-cmd:hover { background: var(--bg-card); }

.tx-cmd.pass { border-left-color: var(--pass); }
.tx-cmd.fail { border-left-color: var(--fail); }
.tx-cmd.warn { border-left-color: var(--warn); }
.tx-cmd.pending { border-left-color: var(--warn); opacity: 0.7; }
.tx-cmd.info { border-left-color: transparent; }

.tx-cmd-icon {
width: 13px;
text-align: center;
Expand Down
2 changes: 1 addition & 1 deletion test/specs/ui5.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test.describe('UI5', () => {
await page.locator('[data-sap-ui="container-cart---checkoutView--deliveryTypeStep-nextButton"]').click();
await expect(page.locator('#container-cart---checkoutView--totalPriceTitle-inner')).toHaveText('Total: 16,00 EUR');
await page.locator('#container-cart---checkoutView--submitOrder').click();
await page.locator('footer button:has-text("Yes")').click();
await page.locator('footer button').filter({ hasText: 'Yes' }).click();
});

await step('Verify order confirmation', async () => {
Expand Down
34 changes: 5 additions & 29 deletions test/unit/locator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,15 @@ describe('textMatches', () => {
// ── resolveSelector ───────────────────────────────────────────────────────────

describe('resolveSelector', () => {
test('returns plain selector with null hasText', () => {
const result = resolveSelector('button');
assert.deepEqual(result, [{ base: 'button', hasText: null }]);
});

test('extracts double-quoted :has-text() filter', () => {
const result = resolveSelector('button:has-text("Submit")');
assert.deepEqual(result, [{ base: 'button', hasText: 'Submit' }]);
});

test('extracts single-quoted :has-text() filter', () => {
const result = resolveSelector("div:has-text('click me')");
assert.deepEqual(result, [{ base: 'div', hasText: 'click me' }]);
});

test('uses * as base when only :has-text() is present', () => {
const result = resolveSelector(':has-text("Submit")');
assert.deepEqual(result, [{ base: '*', hasText: 'Submit' }]);
test('returns single selector as array', () => {
assert.deepEqual(resolveSelector('button'), ['button']);
});

test('splits comma-separated selectors', () => {
const result = resolveSelector('input, button');
assert.deepEqual(result, [
{ base: 'input', hasText: null },
{ base: 'button', hasText: null },
]);
assert.deepEqual(resolveSelector('input, button'), ['input', 'button']);
});

test('handles mixed selectors with and without has-text', () => {
const result = resolveSelector('span:has-text("ok"), div');
assert.deepEqual(result, [
{ base: 'span', hasText: 'ok' },
{ base: 'div', hasText: null },
]);
test('trims whitespace around each part', () => {
assert.deepEqual(resolveSelector(' span , div '), ['span', 'div']);
});
});
Loading