gemini-testing / testplane

Testplane (ex-hermione) browser test runner based on mocha and wdio
https://testplane.io
MIT License
687 stars 62 forks source link

Not clear how to create wrapper and fail test with browser.mock #910

Closed olegKusov closed 3 months ago

olegKusov commented 4 months ago

Verify latest release

Hermione version

8.0.4

Last Hermione version that worked

No response

Which area(s) of Hermione are affected? (leave empty if unsure)

No response

Link to the code that reproduces this issue or a replay of the bug

No response

Reproduction steps

I want to create custom command for browser.mock


module.exports = async function (url, cb, opts) {
    return new Promise(async (resolve, reject) => {
        const mock = await this.mock(url, {fetchResponse: false});
        mock.respond((request) => {
            try {
                 resovle();
                return cb(request);
            } catch(e) {
                reject(e);
            }
        }, {
            fetchResponse: false,
            ...opts,
        })
    });
};

The problem is that its not clear how to make it work.

Actual Behavior

browser.addCommand('loadPageWithout404', (url, {selector, predicate}) => new Promise(async (resolve, reject) => {
    const mock = await this.mock('**')

    mock.on('match', ({url, statusCode}) => {
        if (statusCode === 404) {
            reject(new Error(`request to ${url} failed with "Not Found"`))
        }
    })

    await this.url(url).catch(reject)

    // waiting here, because some requests can still be pending
    if (selector) {
        await this.$(selector).waitForExist().catch(reject)
    }

    if (predicate) {
        await this.waitUntil(predicate).catch(reject)
    }

    resolve()
}))

In webdriverio docs there is one example. And as far as I understand they waiting for all requests are done and only after that they can be sure that mock.on will be called. But its very strange logic. What if i click to button in my test after page load and request will be 404. then this command will no work (e.g. it will not fail test). And with this logic we need to create mock every time we making some actions and not at the top level of test how I understand it, e.g

await this.browser.loadWithout404('');

await this.browser.loadWithout404('/button_action_url', {selector: 'some_selector_after_action'});
await this.browser.$(...).click();

One more thing that looks strange to me is that statusCode handled separately. So if we want to mock request with some data and change status Code then we need to duplicate logic

        await this.browser.myMockCommand('**' + '/api/...', (req) => {
            if(req.postData !== JSON.stringify(mock)) {
                      return {res: 'body is wrong'};
            }
        }, {statusCode: (data) =>  req.postData !== JSON.stringify(mock) ? 404 : 200}
           })

Expected Behavior

I just want to get how to fail test if mock.respond returns error.

Which Node.js version are you using?

18.12.1

KuznetsovRoman commented 3 months ago

If you want to create a command, then yes, browser command can only resolve and reject, and resolved command can't reject afterwards.

With webdriverio mock callbacks, i can offer you this approach:

In code, it would look like this:

// .testplane.conf.ts
browser.mockRestoreAll()

module.exports = {
    plugins: {
        '@testplane/global-hook': {
            beforeEach: async ({browser}) => {
                const networkSpy = await browser.mock("**"); // create network spy

                networkSpy.on('match', async ({statusCode, url}) => {
                    if (statusCode === 404) {
                        await browser.setMeta("notFoundUrl", url); // set meta property to check if in afterEach
                        // Note: we can't reject or throw errors inside "on" callback, because it would produce unhandled rejection
                    }
                });
            },
            afterEach: async ({browser}) => {
                await browser.mockRestoreAll(); // unsubscribe to browser mocks

                const notFoundUrl = await browser.getMeta("notFoundUrl");

                if (notFoundUrl) {
                    // but we can throw an error here
                    throw new Error("Not found at " + url);
                }
            }
        },
        // other testplane plugins
    },

    // other testplane settings...
};

As i mentioned, we can't throw errors inside of browserMock.on callback, as well as we can't just interrupt test body execution from somewhere outside of it.

If you want to, you can overwrite "url" command so it would check for browser.getMeta("notFoundUrl") after "url" resolve, but for "click" part it is much harder. All we can see is:

We can't tell if network request was called because of click. Click itself could be the reason and could not be the reason of that network request. Even wrapping "url" will not 100% help you because it only waits for document.readyState to be complete, but you might have some lazy network requests.

With the url part, i can offer you openAndWait command: https://github.com/gemini-testing/testplane/blob/master/docs/writing-tests.md#openandwait

It would wait for page to load and check for network errors. Notice how you still need to specify waitNetworkIdleTimeout, because there could be a gap between one request resolved and the second one started (for example, when your react bundle just loaded, it needs to be processed, and only then your fetch in useEffect is called)

DudaGod commented 3 months ago

I am closing it due to inactivity.