intern icon indicating copy to clipboard operation
intern copied to clipboard

Add locator helper

Open bryanforbes opened this issue 8 years ago • 7 comments

Currently, to use different location strategies, one must either call find() with the strategy as the first argument or use the find*() methods (findByCssSelector(), etc.). While this works, a more robust approach might be to pass an object to find() which would be created by a helper function. Consider:

import { By } from ‘leadfoot’;

describe(() => {
    it(async ({ remote }) => {
        const element = await remote.find(By.css(‘.my-class’));
    });
});

By would have functions like css(), className(), xpath(), etc. which would return objects that find() would use to set the strategy and value (perhaps something like { strategy: string; value: string; }). The upside of this approach is that the locators could be reused fairly easily.

bryanforbes avatar Jul 15 '17 02:07 bryanforbes

I like the idea of locator reuse (so the API is less repetitive) that doesn't involve string literals (because...string literals). I do think we might lose some ease-of-use, though; from a discoverability perspective, findElementById is arguably easier to arrive at than find(someLocator, value), which is easier than (in my mind) import {someLocatorFunction} from 'leadfoot/locators'; find(someLocatorFunction(value)).

jason0x43 avatar Jul 15 '17 14:07 jason0x43

Or I could be crazy. I'm paranoid about IDE discoverability now. :)

jason0x43 avatar Jul 15 '17 15:07 jason0x43

I’m not opposed to leaving find(strategy: string, value: string) as long as the strategy values are typed in the .d.ts file (they aren’t currently). But the reusability of selectors (and the potential for extensibility) could be powerful:

const { describe, it, before } = intern.getPlugin('interface.bdd');
const { expect } = intern.getPlugin('chai');

import { By } from ‘leadfoot’;
import { Remote } from 'intern/lib/executors/Node';

class AppPage {
    private parent = By.css(‘my-app’);

    nav = this.parent.css(‘> nav’);
    dashboard = this.parent.css(‘> dashboard’);

    constructor(private remote: Remote) {}

    async load() {
    	const { remote } = this;
    	await remote.setFindTimeout(5000);
    	await remote.get('dist/index.html');
    	await this.waitForApp();
    }

    async getNav() {
        return await this.remote.find(this.nav);
    }
    async getDashboard {
        return await this.remote.find(this.dashboard);
    }

    async waitForApp() {
        return await this.remote.findDisplayed(this.parent);
    }
    async waitForNav() {
        return await this.remote.findDisplayed(this.nav);
    }
    async waitForDashboard() {
        return await this.remote.findDisplayed(this.dashboard);
    }
}

describe(‘App’, () => {
    let app: AppPage;

    before(async ({ remote }) => {
        app = new AppPage(remote);
        await app.load();
    });

    it(‘should have a dashboard and nav’, async ({ remote }) => {
        expect(await app.getDashboard()).to.exist;
        expect(await app.getNav()).to.exist;

        const links = await remote.findAll(app.nav.css(‘> a[routerLink]’));
        expect(links).to.have.lengthOf(3);
    });
});

bryanforbes avatar Jul 15 '17 17:07 bryanforbes

I came up with a pretty simple proof of concept today and updated some functional tests to use it. Let me know what you think.

bryanforbes avatar Jul 15 '17 19:07 bryanforbes

This is looking pretty nice.

Just out of curiosity, why a By constant vs just exporting locator functions from a by or locator module?

import { css } from 'leadfoot/locators';
// ...
private parent = css('my-app');

jason0x43 avatar Jul 16 '17 13:07 jason0x43

I was simulating what protractor has, but we don’t need to do that. I think having it in a By constant (or something similarly named) makes it read better:

remote.find(By.css('.thing');

// vs

remote.find(css('.thing'));

bryanforbes avatar Jul 16 '17 20:07 bryanforbes

Hmm... I suppose it does.

jason0x43 avatar Jul 17 '17 02:07 jason0x43