How to use PageObject in the tab which has been created during test run? - javascript

When I try to use my pageObjects in a newly created tab (during the test run), the test tries to interact with the base page.
Working with a newly created tab is working as long as I use const = [newPage], eg. newPage.locator('someLocator').click().
I want to avoid using detailed actions in the test, I just want to make function in the pageObject and reuse it with newPage.
my code:
pageObject:
export class SharedPage {
/**
* #param {import('#playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.addToCartButton = page.locator('text=Add to cart');
}
async addToCartButtonClick() {
await this.addToCartButton.click();
}
}
Test:
import { test } from '#playwright/test';
import { LoginPage } from '../../pages/LoginPage';
import { ProductsPage } from '../../pages/ProductsPage';
import { SharedPage } from '../../pages/SharedPage';
const newPageLocators = {
sauceLabsOnesie: 'text=Sauce Labs Onesie',
sauceLabsBackpack: 'Sauce Labs Backpack',
};
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goTo('inventory.html');
});
test('As a user I want to open a new tab and visit it', async ({
page,
context,
}) => {
const productsPage = new ProductsPage(page);
const [newPage] = await Promise.all([
context.waitForEvent('page'),
productsPage.openNewTabWithProduct(newPageLocators.sauceLabsBackpack),
]);
const sharedPage = new SharedPage(newPage);
productsPage.selectProductByText(newPage, newPageLocators.sauceLabsOnesie);
newPage.locator(newPageLocators.sauceLabsOnesie).click(); // it works
sharedPage.addToCartButtonClick(); // it doesn't work, test tries to perform this step on the base page
});

The fundamental problem is missing awaits in front of asynchronous Playwright API calls as well as your custom wrappers on them:
// ...
const sharedPage = new SharedPage(newPage);
await productsPage.selectProductByText(newPage, newPageLocators.sauceLabsOnesie);
await newPage.locator(newPageLocators.sauceLabsOnesie).click();
await sharedPage.addToCartButtonClick();
// ...
For your constructor, async/await isn't possible. See Async/Await Class Constructor. Luckily, Playwright lets you chain locator().click() with a single await so your class code looks OK for now, but it's something to bear in mind as you add more code.

Related

NodeJS: My jest spyOn function is not being called

I don't understand why my spy is not being used. I have used this code elsewhere and it has worked fine.
Here is my test:
const {DocumentEngine} = require('../documentEngine')
const fileUtils = require('../utils/fileUtils')
const request = {...}
const fieldConfig = {...}
test('If the Carbone addons file is not found, context is built with the carboneAddons property as an empty object', async () => {
const expectedResult = {
carboneAddons: {},
}
const fileExistSpy = jest
.spyOn(fileUtils, 'checkFileExists')
.mockResolvedValue(false)
const result = await docEngine.buildContext(request, fieldConfig)
expect(fileExistSpy).toHaveBeenCalledTimes(1)
})
Here is the code that it is being tested:
async function buildContextForLocalResources(request, fieldConfig) {
/* other code */
const addonFormatters = await getCarboneAddonFormatters()
const context = {
sourceJson,
addonFormatters,
documentFormat,
documentTemplateId,
documentTemplateFile,
responseType,
jsonTransformContext
}
return context
}
async function getCarboneAddonFormatters() {
const addOnPath = path.resolve(
docConfig.DOC_GEN_RESOURCE_LOCATION,
'library/addon-formatters.js'
)
if (await checkFileExists(addOnPath)) {
logger.info('Formatters found and are being used')
const {formatters} = require(addOnPath)
return formatters
}
logger.info('No formatters were found')
return {}
}
This is the code from my fileUtils file:
const fs = require('fs/promises')
async function checkFileExists(filePath) {
try {
await fs.stat(filePath)
return true
} catch (e) {
return false
}
}
My DocumentEngine class calls the buildContext function which in turn calls the its method getCarboneAddonFormatters. The fileUtils is outside of DocumentEngine class in a utilities folder. The original code I had this working on was TypeScript as opposed to this which is just NodeJS Javascript. The config files for both are the same. When I try to step through the code (VSCode debugger), as soon as I hit the line with await fs.stat(filePath) in the checkFileExists function, it kicks me out of the test and moves on to the next test - no error messages or warnings.
I've spent most of the day trying to figure this out. I don't think I need to do an instance wrapper for the documentEngine, because checkFileExists is not a class member, and that looks like a React thing...
Any help in getting this to work would be appreciated.

Creating global Destructor to use it in another code blocks

Im recently learning JavaScript and TypeScript and trying to create a test automation framework with Playwright and having a small issue.
At " test.only " block, I have created the destructor. But I want to make this destructor global so I can use it in other test code blocks so I don't need to keep create a Destructor for every test block. I tried to create it on test.beforeEach code block but it gave an error and didn't allow me to use in other blocks. Is there any way or idea to solve this issue so I can just create Destructor once and use it in all code blocks ?
Thank you very much!
import { FeedbackPage } from '../../pages/FeedbackPage';
import { HomePage } from '../../pages/HomePage';
test.describe('Feedback Form', () => {
let homePage: HomePage;
let feedbackPage: FeedbackPage;
test.beforeEach(async ({ page }) => {
homePage = new HomePage(page);
feedbackPage = new FeedbackPage(page);
await homePage.visit();
await homePage.clickOnFeedbackLink();
});
test.only('Submit feedback form', async ({ page }) => {
const {nameInput, emailInput, subjectInput, commentInput} = feedbackPage;
// await feedbackPage.fillForm(
// 'name',
// 'email#email.com',
// 'subject',
// 'my awesome message'
// );
await nameInput.type('Name');
await emailInput.type('email#email.com');
await subjectInput.type('subject');
await commentInput.type('my awesome message');
await page.pause()
await feedbackPage.submitForm();
await feedbackPage.feedbackFormSent();
});
});```

How to refactor by eliminating double await and the DRY code?

I'm having doubts about which is the best strategy to manage the many service clients in this web app.
"Best" in terms of a good compromise between user's device RAM and Javascript execution speed (main thread ops).
This is what I'm doing right now, this is the main file:
main.ts:
import type { PlayerServiceClient } from './player.client';
import type { TeamServiceClient } from './team.client';
import type { RefereeServiceClient } from './referee.client';
import type { FriendServiceClient } from './friend.client';
import type { PrizeServiceClient } from './prize.client';
import type { WinnerServiceClient } from './winner.client';
import type { CalendarServiceClient } from './calendar.client';
let playerService: PlayerServiceClient;
export const player = async (): Promise<PlayerServiceClient> =>
playerService ||
((playerService = new (await import('./player.client')).PlayerServiceClient()),
playerService);
let teamService: TeamServiceClient;
export const getTeamService = (): TeamServiceClient =>
teamService ||
((teamService = new (await import('./team.client')).TeamServiceClient()),
teamService);
let refereeService: RefereeServiceClient;
export const getRefereeService = (): RefereeServiceClient =>
refereeService ||
((refereeService = new (await import('./referee.client')).RefereeServiceClient()),
refereeService);
let friendService: FriendServiceClient;
export const getFriendService = (): FriendServiceClient =>
friendService ||
((friendService = new (await import('./friend.client')).FriendServiceClient()),
friendService);
let prizeService: PrizeServiceClient;
export const getPrizeService = (): PrizeServiceClient =>
prizeService ||
((prizeService = new (await import('./prize.client')).PrizeServiceClient()),
prizeService);
let winnerService: WinnerServiceClient;
export const getWinnerService = (): WinnerServiceClient =>
winnerService ||
((winnerService = new (await import('./winner.client')).WinnerServiceClient()),
winnerService);
let calendarService: CalendarServiceClient;
export const getCalendarService = (): CalendarServiceClient =>
calendarService ||
((calendarService = new (await import('./calendar.client')).CalendarServiceClient()),
calendarService);
// and so on... a lot more...
As you can see there are many service clients.
I'm using this code because I thought it was better given my web app structure based on routes almost overlapping with client services:
I mean, if the player goes from /home to /players page I can use it like this:
components/players.svelte
import { getPlayerService } from "main";
const playerService = await getPlayerService();
const players = await playerService.queryPlayers();
In this way, if the PlayerService does not exist, it is imported at the moment and returned, otherwise it returns the one imported and instantiated before.
Since the user switches pages frequently this way I can avoid the sudden creation and destruction of those clients, right?
But in this way I am using global variables which I don't like to use and I'm using verbose, DRY and long code in each component.
Is there a way to use the below code in components instead?
import { playerService } from "main";
const players = await playerService.queryPlayers();
What do you suggest me to do?
The patterns you are implementing are "lazy loading" and "singleton".
You could have a single service factory which implements those patterns and use it for every service:
File serviceFactory.js
const serviceMap = {};
export function getService(serviceName) {
return serviceMap[serviceName] ?? (serviceMap[serviceName] = import(serviceName).then(x => new x.default));
}
The ECMAScript modules standard will take care of executing the serviceFactory.js code only once in the application (no matter how many times you import it), so you can hold the singletons in a map assigned to a private top-level variable of the serviceFactory.js module.
This service factory implies that every service is exported with the default keyword like that:
export default class SomeService {
constructor() {
// ...
}
fetchSomething() {
// ...
}
}
Then, use the services everywhere in your application with this code:
import { getService } from './serviceFactory.js';
const service = await getService('./services/some.service.js');
const something = await service.fetchSomething();
If you really want to remove the double await, you can encapsulate it in the service factory like that:
const serviceMap = {};
export function getService(serviceName) {
return serviceMap[serviceName] ?? (serviceMap[serviceName] = resolveService(serviceName));
}
function resolveService(name) {
const futureInstance = import(name).then(x => new x.default);
const handler = {
get: function (target, prop) {
return function (...args) {
return target.then(instance => instance[prop](...args));
}
}
}
return new Proxy(futureInstance, handler);
}
Which allows you to write this code:
const something = await getService('./services/some.service.js').fetchSomething();
This allows the service to be loaded at the exact line of code where you need it.
If it doesn't bothers you to load it with a static import because you need the import { playerService } from "main"; syntax, you can expose every service like this in one file per service:
export const playerService = getService('./services/player.service.js');
I have published the full working demo here: https://github.com/Guerric-P/lazy-singletons-demo
I don't think the code from #Guerric will work with build tools (like webpack.)
Specifically dynamic string imports import(modulePath) is not supported.
My recommendation is to reduce the repeating bits of code to their smallest representation... Hopefully, it'll end up feeling less noisy.
Solution #1/2
Here's an example using a higher-order memoize function to help with the caching.
// Minimal definition of service loaders
export const getPlayerService = memoize<PlayerServiceClient>(async () => new (await import('./player.client')).PlayerServiceClient());
export const getTeamService = memoize<TeamServiceClient>(async () => new (await import('./team.client')).TeamServiceClient());
export const getRefereeService = memoize<RefereeServiceClient>(async () => new (await import('./referee.client')).RefereeServiceClient());
export const getFriendService = memoize<FriendServiceClient>(async () => new (await import('./friend.client')).FriendServiceClient());
export const getPrizeService = memoize<PrizeServiceClient>(async () => new (await import('./prize.client')).PrizeServiceClient());
export const getWinnerService = memoize<WinnerServiceClient>(async () => new (await import('./winner.client')).WinnerServiceClient());
// Mock hacked together memoize fn
// TODO: Replace with some npm library alternative
const fnCache = new WeakMap();
function memoize<TReturn>(fn): TReturn {
let cachedValue = fnCache.get(fn);
if (cachedValue) return cachedValue;
cachedValue = fn();
fnCache.set(fn, cachedValue);
return cachedValue;
}
Solution #2/2
Depending on the version of the JS engine & transpiler, you could possibly cut out some code and use the nature of modules to cache singletons of your services.
(Note: I've occasionally run into gotchas here around how ES Modules rely on deterministic exports. The workaround is to assign the exports to pending promises which return the instance.)
The important feature to know about Promises: they are only resolved once, and can be used to effectively cache their result.
Each await or .then will get the initial resolved value.
// SUPER minimal definition of services
export const playerService = (async (): PlayerServiceClient => new (await import('./player.client')).PlayerServiceClient())();
export const teamService = (async (): TeamServiceClient => new (await import('./team.client')).TeamServiceClient())();
export const refereeService = (async (): RefereeServiceClient => new (await import('./referee.client')).RefereeServiceClient())();
export const friendService = (async (): FriendServiceClient => new (await import('./friend.client')).FriendServiceClient())();
export const prizeService = (async (): PrizeServiceClient => new (await import('./prize.client')).PrizeServiceClient())();
export const winnerService = (async (): WinnerServiceClient => new (await import('./winner.client')).WinnerServiceClient())();
Calling the Service Wrapper
import { playerService } from "./services";
// Example: Using async/await IIFE
const PlayerService = (async () => await playerService)();
function async App() {
// Example: Function-scoped service instance:
// const PlayerService = await playerService
const players = await PlayerService.queryPlayers();
}

testcafe running different tests based on browser

I was wondering if there is a way to somehow pass a parameter to let your fixture or even all tests know which browser they are running in.
In my particular case, I would use that parameter to simply assign a corresponding value to a variable inside my tests.
For example,
switch(browser) {
case 'chrome':
chrome = 'chrome.com';
break;
case 'firefox':
link = 'firefox.com';
break;
case 'safari':
link = 'safari.com';
break;
default:
break;
}
Currently, I was able to achieve something similar by adding a global node variable and it looks something like this:
"chrome": "BROWSER=1 node runner.js"
However, this makes me create a separate runner for every browser (safari-runner, chrome-runner etc.) and I would like to have everything in one place.
So at the end of the day, I would need to make this work:
const createTestCafe = require('testcafe');
let testcafe = null;
createTestCafe('localhost', 1337, 1338)
.then(tc => {
testcafe = tc;
const runner = testcafe.createRunner();
return runner
.src('test.js')
.browsers(['all browsers'])
.run({
passBrowserId: true // I guess it would look something like this
});
})
.then(failedCount => {
console.log('Tests failed: ' + failedCount);
testcafe.close();
})
.catch(error => {
console.log(error);
testcafe.close();
});
There are several ways to get browser info:
Get navigator.userAgent from the browser using ClientFunction. Optionally you can use a module to parse an user agent string, for example: ua-parser-js.
import { ClientFunction } from 'testcafe';
import uaParser from 'ua-parser-js';
fixture `get ua`
.page `https://testcafe.devexpress.com/`;
const getUA = ClientFunction(() => navigator.userAgent);
test('get ua', async t => {
const ua = await getUA();
console.log(uaParser(ua).browser.name);
});
Use RequestLogger to obtain browser info. For example:
import { RequestLogger } from 'testcafe';
const logger = RequestLogger('https://testcafe.devexpress.com/');
fixture `test`
.page('https://testcafe.devexpress.com')
.requestHooks(logger);
test('test 1', async t => {
await t.expect(logger.contains(record => record.response.statusCode === 200)).ok();
const logRecord = logger.requests[0];
console.log(logRecord.userAgent);
});
The TestCafe team is working on the t.browserInfo function, which solves the issue in the future.
Just to update this question, testcafe has now implemented t.browser, which will allow you to check the browser.name or browser.alias to determine which browser you're running in.
import { t } from 'testcafe';
const browserIncludes = b => t.browser.name.includes(b);
const isBrowserStack = () => t.browser.alias.includes('browserstack');
fixture `test`
.page('https://testcafe.devexpress.com')
test('is Chrome?', async () => {
console.log(t.browser.name);
await t.expect(browserIncludes('Chrome').ok();
await t.expect(isBrowserStack()).notOk();
});

Use Jest to mock external user module in an imported module

I don't know if I'm missing something in the docs, but I have this situation:
// test.js
import User from './user'
it("should load initial data", async() => {
const users = new User()
const user = await users.load()
})
// User.js
import Api from './api'
export default class User {
async load() {
const res = await Api.fetch() // prevent/mock this in testing
}
}
What is the Jest-way to prevent/mock the external Api module in User.js. I do not want User.js to make a real network request within the test.
Further to this, I'm looking for a more generic mocking solution, ie. say I'm testing in React Native, and I want to mock NativeModules.SettingsManager.settings.AppleLocale, for example. Lets say Api.fetch() calls the line above, and doesn't make a HTTP request
spyOn in combination with mock functions like mockImplementation will provide what you are looking for.
Here is a working example:
// ---- api.js ----
export const getData = () => {
return Promise.resolve('hi');
}
// ---- user.js ----
import { getData } from './api'
export default class User {
async load() {
return await getData(); // mock this call in user.test.js
}
}
// ---- user.test.js ----
import User from './user'
import * as Api from './api'; // import * so we can mock 'getData' on Api object
describe('User', () => {
it('should load initial data', async() => {
const mock = jest.spyOn(Api, 'getData'); // create a spy
mock.mockImplementation(() => Promise.resolve('hello')); // give it a mock implementation
const user = new User();
const result = await user.load();
expect(result).toBe('hello'); // SUCCESS, mock implementation called
mock.mockRestore(); // restore original implementation when we are done
});
});
If you need to mock responses to HTTP requests, then you should check out nock. It has a clean API that allows a lot of flexibility in creating HTTP responses to specific requests.

Categories

Resources