Skip to content

Commit

Permalink
fix(mock-doc): improve error message when :scope selector is used (#…
Browse files Browse the repository at this point in the history
…5318)

This improves the error message shown when there is an error on the
jQuery side when running `querySelector` and friends by selectively
making an addendum to the error message to just explain a bit better
what's going on.

In particular, we know that the `:scope`, `:where`, and `:is`
pseudo-selectors aren't supported at present in jQuery, so we can detect
if the selector passed to our mock-doc impls of `querySelector` and co
included any of those selectors and, if so, add a message with some
specific info about what's going on and what to do.

STENCIL-1108
  • Loading branch information
alicewriteswrongs committed Feb 2, 2024
1 parent f498f09 commit f5d4e98
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 7 deletions.
94 changes: 87 additions & 7 deletions src/mock-doc/selector.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,96 @@
import { MockElement } from './node';
import jQuery from './third-party/jquery';

export function matches(selector: string, elm: MockElement) {
const r = jQuery.find(selector, undefined, undefined, [elm]);
return r.length > 0;
/**
* Check whether an element of interest matches a given selector.
*
* @param selector the selector of interest
* @param elm an element within which to find matching elements
* @returns whether the element matches the selector
*/
export function matches(selector: string, elm: MockElement): boolean {
try {
const r = jQuery.find(selector, undefined, undefined, [elm]);
return r.length > 0;
} catch (e) {
updateSelectorError(selector, e);
throw e;
}
}

/**
* Select the first element that matches a given selector
*
* @param selector the selector of interest
* @param elm the element within which to find a matching element
* @returns the first matching element, or null if none is found
*/
export function selectOne(selector: string, elm: MockElement) {
const r = jQuery.find(selector, elm, undefined, undefined);
return r[0] || null;
try {
const r = jQuery.find(selector, elm, undefined, undefined);
return r[0] || null;
} catch (e) {
updateSelectorError(selector, e);
throw e;
}
}

export function selectAll(selector: string, elm: MockElement) {
return jQuery.find(selector, elm, undefined, undefined);
/**
* Select all elements that match a given selector
*
* @param selector the selector of interest
* @param elm an element within which to find matching elements
* @returns all matching elements
*/
export function selectAll(selector: string, elm: MockElement): any {
try {
return jQuery.find(selector, elm, undefined, undefined);
} catch (e) {
updateSelectorError(selector, e);
throw e;
}
}

/**
* A manifest of selectors which are known to be problematic in jQuery. See
* here to track implementation and support:
* https://github.com/jquery/jquery/issues/5111
*/
export const PROBLEMATIC_SELECTORS = [':scope', ':where', ':is'] as const;

/**
* Given a selector and an error object thrown by jQuery, annotate the
* error's message to add some context as to the probable reason for the error.
* In particular, if the selector includes a selector which is known to be
* unsupported in jQuery, then we know that was likely the cause of the
* error.
*
* @param selector our selector of interest
* @param e an error object that was thrown in the course of using jQuery
*/
function updateSelectorError(selector: string, e: unknown) {
const selectorsPresent = PROBLEMATIC_SELECTORS.filter((s) => selector.includes(s));

if (selectorsPresent.length > 0 && (e as Error).message) {
(e as Error).message =
`At present jQuery does not support the ${humanReadableList(selectorsPresent)} ${selectorsPresent.length === 1 ? 'selector' : 'selectors'}.
If you need this in your test, consider writing an end-to-end test instead.\n` + (e as Error).message;
}
}

/**
* Format a list of strings in a 'human readable' way.
*
* - If one string (['string']), return 'string'
* - If two strings (['a', 'b']), return 'a and b'
* - If three or more (['a', 'b', 'c']), return 'a, b and c'
*
* @param items a list of strings to format
* @returns a formatted string
*/
function humanReadableList(items: string[]): string {
if (items.length <= 1) {
return items.join('');
}
return `${items.slice(0, items.length - 1).join(', ')} and ${items[items.length - 1]}`;
}
48 changes: 48 additions & 0 deletions src/mock-doc/test/selector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MockDocument } from '../document';
import { MockElement } from '../node';
import { PROBLEMATIC_SELECTORS } from '../selector';

describe('selector', () => {
it('closest', () => {
Expand Down Expand Up @@ -225,4 +226,51 @@ describe('selector', () => {
const q2 = div.querySelector('span.c a');
expect(q2.tagName).toBe('A');
});

it.each(PROBLEMATIC_SELECTORS)("should error for '%p' selector", (selector) => {
const doc = new MockDocument();

const expectedMessage = [
`At present jQuery does not support the ${selector} selector.`,
'If you need this in your test, consider writing an end-to-end test instead.',
`Syntax error, unrecognized expression: unsupported pseudo: ${selector.replace(':', '')}`,
].join('\n');

expect(() => doc.querySelector(selector)).toThrow(expectedMessage);
expect(() => doc.querySelectorAll(selector)).toThrow(expectedMessage);
expect(() => doc.matches(selector)).toThrow(expectedMessage);
});

it('should error for combinations of problematic selectors', () => {
const doc = new MockDocument();
expect(() => {
doc.querySelector(':scope :is');
}).toThrow(
[
`At present jQuery does not support the :scope and :is selectors.`,
'If you need this in your test, consider writing an end-to-end test instead.',
`Syntax error, unrecognized expression: unsupported pseudo: scope`,
].join('\n'),
);

expect(() => {
doc.querySelector(':is :where');
}).toThrow(
[
`At present jQuery does not support the :where and :is selectors.`,
'If you need this in your test, consider writing an end-to-end test instead.',
`Syntax error, unrecognized expression: unsupported pseudo: is`,
].join('\n'),
);

expect(() => {
doc.querySelector(':scope :is :where');
}).toThrow(
[
`At present jQuery does not support the :scope, :where and :is selectors.`,
'If you need this in your test, consider writing an end-to-end test instead.',
`Syntax error, unrecognized expression: unsupported pseudo: scope`,
].join('\n'),
);
});
});

0 comments on commit f5d4e98

Please sign in to comment.