Skip to content

Commit d822787

Browse files
authored
feat: 'within' using another query (#34)
* feat: within using another querty * within for. 'All' queries * supports regex in within queries * functions in within queries
1 parent f7606b2 commit d822787

File tree

5 files changed

+126
-21
lines changed

5 files changed

+126
-21
lines changed

.testcaferc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"chrome:headless",
88
"firefox:headless"
99
],
10-
"concurrency": 1
10+
"concurrency": 3
1111
}

src/index.js

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { ClientFunction, Selector } from 'testcafe'
44
import { queries } from '@testing-library/dom'
55

6+
const SELECTOR_TYPE = (Selector(new Function())()).constructor.name;
7+
68

79

810
export async function configureOnce(options) {
@@ -25,28 +27,92 @@ Object.keys(queries).forEach(queryName => {
2527
module.exports[queryName] = Selector(
2628
new Function(
2729
`
28-
return TestingLibraryDom.${queryName}(document.body, ...arguments);
30+
if(!window.tctlReplacer) {
31+
window.tctlReplacer = function tctlReplacer(key, value) {
32+
if (value instanceof RegExp)
33+
return ("__REGEXP " + value.toString());
34+
else if (typeof value === 'function')
35+
return ("__FUNCTION " + value.toString());
36+
else
37+
return value;
38+
}
39+
}
40+
const els = TestingLibraryDom.${queryName}(document.body, ...arguments);
41+
if(!Array.isArray(els)) {
42+
els.setAttribute('data-tctl-args', JSON.stringify(Array.from(arguments), window.tctlReplacer, 0));
43+
els.setAttribute('data-tctl-queryname', '${queryName}');
44+
} else {
45+
els.forEach((el,i) => {
46+
el.setAttribute('data-tctl-args', JSON.stringify(Array.from(arguments), window.tctlReplacer, 0));
47+
el.setAttribute('data-tctl-queryname', '${queryName}');
48+
el.setAttribute('data-tctl-index', i);
49+
});
50+
}
51+
return els;
2952
`,
3053
),
31-
)
54+
);
3255
})
56+
function reviver(key, value) {
57+
if (value.toString().indexOf('__REGEXP ') == 0) {
58+
const m = value.split('__REGEXP ')[1].match(/\/(.*)\/(.*)?/);
59+
return new RegExp(m[1], m[2] || '');
60+
} else
61+
return value;
62+
}
63+
64+
export const within = async selector => {
65+
if (selector instanceof Function) {
66+
return within(selector());
67+
}
68+
69+
if (selector.constructor.name === SELECTOR_TYPE) {
70+
const el = await selector;
71+
const withinQueryName = el.getAttribute('data-tctl-queryname');
3372

34-
export const within = selector => {
35-
const sanitizedSelector = selector.replace(/"/g, "'")
73+
const withinArgs = JSON.parse(el.getAttribute('data-tctl-args'), reviver)
74+
.map(arg => {
75+
if (arg instanceof RegExp) {
76+
return arg.toString();
77+
} else if (arg.toString().indexOf('__FUNCTION ') == 0) {
78+
return (arg.replace('__FUNCTION ', ''))
79+
} else {
80+
return JSON.stringify(arg);
81+
}
82+
}).join(', ');
83+
84+
const withinIndexer = el.hasAttribute('data-tctl-index') ? `[${el.getAttribute('data-tctl-index')}]` : '';
85+
86+
const withinSelectors = {};
87+
Object.keys(queries).forEach(queryName => {
88+
withinSelectors[queryName] = Selector(
89+
new Function(`
90+
91+
const {within, ${withinQueryName}} = TestingLibraryDom;
92+
const el = ${withinQueryName}(document.body, ${withinArgs})${withinIndexer};
93+
return within(el).${queryName}(...arguments);
94+
`
95+
));
96+
});
97+
return withinSelectors;
98+
} else if (typeof (selector) === 'string') {
99+
const sanitizedSelector = selector.replace(/"/g, "'");
36100

37-
const container = {}
101+
const withinSelectors = {};
38102

39-
Object.keys(queries).forEach(queryName => {
40-
container[queryName] = Selector(
41-
new Function(
42-
`
43-
window.TestcafeTestingLibrary = window.TestcafeTestingLibrary || {}
44-
window.TestcafeTestingLibrary["within_${sanitizedSelector}"] = window.TestcafeTestingLibrary["within_${sanitizedSelector}"] || TestingLibraryDom.within(document.querySelector("${sanitizedSelector}"))
45-
return window.TestcafeTestingLibrary["within_${sanitizedSelector}"].${queryName}(...arguments)`,
46-
),
47-
)
48-
})
103+
Object.keys(queries).forEach(queryName => {
104+
withinSelectors[queryName] = Selector(
105+
new Function(
106+
`
107+
const {within} = TestingLibraryDom;
108+
return within(document.querySelector("${sanitizedSelector}")).${queryName}(...arguments);
109+
`),
110+
)
111+
})
49112

50-
return container
113+
return withinSelectors;
114+
} else {
115+
throw new Error(`"within" only accepts a string or another testing-library query as a parameter. ${selector} is not one of those`)
116+
}
51117
}
52118

test-app/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ <h2>getByPlaceholderText</h2>
3232
<section>
3333
<h2>getByText</h2>
3434
<button onclick="this.innerText = 'Button Clicked'">Button Text</button>
35-
<div id="nested">
35+
<div id="nested" data-testid="nested">
36+
<h3>getByText within</h3>
37+
<button onclick="this.innerText = 'Button Clicked'">Button Text</button>
38+
</div>
39+
<div id="nested2" data-testid="nested2">
3640
<h3>getByText within</h3>
3741
<button onclick="this.innerText = 'Button Clicked'">Button Text</button>
3842
</div>

tests/testcafe/within.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Selector } from 'testcafe'
2-
import { within } from '../../src'
2+
// eslint-disable-next-line import/named
3+
import { within, getAllByTestId, getByTestId } from '../../src'
34

45
// eslint-disable-next-line babel/no-unused-expressions
56
fixture`within`
@@ -34,4 +35,38 @@ test('still works after browser page reload', async t => {
3435

3536
await t.eval(() => location.reload(true));
3637
await t.expect(nested.getByText('Button Text').exists).ok()
37-
})
38+
});
39+
40+
41+
test('works with nested selectors', async t => {
42+
const nested = await within(getByTestId('nested'));
43+
await t.expect(nested.getByText('Button Text').exists).ok()
44+
45+
});
46+
47+
test('works with nested selector from "All" query with index - regex', async t => {
48+
const nestedDivs = getAllByTestId(/nested/);
49+
await t.expect(nestedDivs.count).eql(2);
50+
const nested = await within(nestedDivs.nth(0));
51+
52+
await t.expect(nested.getByText('Button Text').exists).ok();
53+
});
54+
55+
test('works with nested selector from "All" query with index - exact:false', async t => {
56+
const nestedDivs = getAllByTestId('nested', { exact: false });
57+
await t.expect(nestedDivs.count).eql(2);
58+
const nested = await within(nestedDivs.nth(0));
59+
60+
await t.expect(nested.getByText('Button Text').exists).ok();
61+
});
62+
63+
test('works with nested selector from "All" query with index - function', async t => {
64+
const nestedDivs = getAllByTestId(
65+
(content, element) =>
66+
element.getAttribute('data-testid').startsWith('nested')
67+
);
68+
await t.expect(nestedDivs.count).eql(2);
69+
const nested = await within(nestedDivs.nth(0));
70+
71+
await t.expect(nested.getByText('Button Text').exists).ok();
72+
});

typings/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function configure(
1212
export type TestcafeBoundFunction<T> = (...params: Parameters<BoundFunction<T>>) => Selector;
1313
export type TestcafeBoundFunctions<T> = { [P in keyof T]: TestcafeBoundFunction<T[P]> };
1414

15-
export function within(selector: string): Promise<TestcafeBoundFunctions<typeof queries>>;
15+
export function within(selector: string | TestcafeBoundFunction): Promise<TestcafeBoundFunctions<typeof queries>>;
1616

1717
export const getByLabelText: TestcafeBoundFunction<typeof queries.getByLabelText>;
1818
export const getAllByLabelText: TestcafeBoundFunction<typeof queries.getAllByLabelText>;

0 commit comments

Comments
 (0)