Egor Chikunov
Egor Chikunov

Reputation: 13

Custom selector to match a node and all descendants

this is TestCafe-specific question. I want to be able to select elements based on the attribute value of the element itself or anywhere in descendants. And the main requirement is to be able to do this without async calls.

Basically, i'm trying to write something like the following:

const findByTestName = (context: Selector, testName: string): Selector => {
    // if context itself matches [data-test~='${testName}'] return context;
    // return context.find('[data-test~='${testName}']');
};

Is it possible?

EDIT: To give a bit more context: I could've tried to solve it using css query like "#someid[data-test='testname'], #someid [data-test='testname']", but i'm trying to create nested page models like so:

class Page {
   constructor() {
     this.root = new Selector('[data-test="page"]');
     this.button = new Button(findByTestName(this.root, 'page__button'));
   }
}

class Button {
   constructor(context: Selector) {
     this.root = findByTestName(context, 'button');
     this.label = findByTestName(this.root, 'button__label');
   }
}

where DOM configuration could be like this:

<div id="page">
  <div data-test="page__button button">
    <span data-test="button__label" />
  </div>
</div>

or this. depending on the actual layout

<div id="page">
  <div data-test="page__button">
    <div data-test="someotherstuff">
      <div data-test="button>
        <span data-test="button__label" />
      </div>
    </div>
  </div>
</div>

this way Button pageModel would be a reusable thing that can be applied to any button.

Upvotes: 1

Views: 340

Answers (2)

Andrey Belym
Andrey Belym

Reputation: 2903

You can pass selectors (even produced by filter functions like Selector.find) as dependencies to create complex selectors synchronously:

import { Selector } from 'testcafe';


const findByTestName = (root, testName) => {
    const children = root.find(`[data-test=${testName}]`);

    const context = { root: root, children: children, testName: testName };

    return Selector(() => {
       const element = root();
       
       if (element.getAttribute('data-test') === testName)
           return el;
       
        return children();

    }, { dependencies: context });
}

fixture`Selector`.page`./test.html`;

test(`Dependencies`, async t => {
      const root = Selector('[data-test=page__button]');
      console.log(await findByTestName(root, "button__label").tagName);
});

Please note that you can get rid of all constants inside the findByTestName function, but shorthand properties cannot be used to specify dependencies:

const findByTestName = (root, testName) => Selector(() => {
        //...
    }, { 
        dependencies: {
            root: root,
            children: root.find(...)
            //...
    }
);

Upvotes: 3

mlosev
mlosev

Reputation: 5227

You need to use the Selector.with method. See an example:

import { Selector } from 'testcafe';

fixture `Fixture`
    .page('./index.html');

const findByTestName = Selector(testName => {
    var checkElement = function (el, checkedAttrValue, result) {
        var testNameAttributeVale = el.getAttribute('data-test');

        if (testNameAttributeVale === checkedAttrValue)
            result.push(el);

        return result;
    };

    var rootElement = context();
    var result      = [];

    do {
        checkElement(rootElement);

        rootElement = rootElement.parentElement;
    }
    while(rootElement);


    return result;

}, { dependencies: { context: Selector('body') }});

test('test', async t => {
    const elms1 = await findByTestName('button');

    const elms2 = await findByTestName.with({
        dependencies: { context: Selector('div[data-test=button]')}
    })('someotherstuff');
});

Upvotes: 2

Related Questions