I'm writing a test to register users on my site. Using #playwright/test, I have defined several different projects in playwright.config.ts:
{
name: 'iPhone 12 Pro',
use: devices['iPhone 12 Pro'],
},
{
name: 'iPhone 12 Pro Max',
use: devices['iPhone 12 Pro Max'],
},
{
name: 'iPhone 5/SE',
use: devices['iPhone SE'],
},
My test looks like this:
test('Register a user', async ({ page }) => {
// Go to baseUrl/webapp/
await page.goto(`${baseUrl}webapp/`);
// Click the register tab.
await page.click('ion-segment-button[role="tab"]:has-text("Register")');
await page.click('input[name="mail"]');
// #TODO: How do I get the project name here?
await page.fill('input[name="mail"]', `test#example.com`);
// Press Tab
await page.press('input[name="mail"]', 'Tab');
await page.fill('input[name="pass"]', 'password');
However, when I run this test, it only works for the first worker because you can only register an email address once.
So what I would like to do is to get access to the project name (for example, iPhone 12 Pro) in my test so that I can convert that to an email address so that each time the test is run, it will register a user based on the project name.
How can I get the project name within a playwright test?
I read the Playwright documentation about the workerInfo object but I can't figure out how to apply it within the test.
You have access to the workerInfo like that as a second parameter, which you can use in that case to make your email unique per worker:
test('Register a user', async ({ page }, workerInfo) => {
// Go to baseUrl/webapp/
await page.goto(`${baseUrl}webapp/`);
// Click the register tab.
await page.click('ion-segment-button[role="tab"]:has-text("Register")');
await page.click('input[name="mail"]');
await page.fill('input[name="mail"]', `test-user-${workerInfo.workerIndex}#example.com`);
You can also get the project name by workerInfo.project.name.
See here for more information.
Nice option is to access testInfo:
test('config projects name', async ({}, testInfo) => {
const projectsName = testInfo.project.name;
expect(projectsName).toEqual('chromium');
});
Tested for config:
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
}
],
Docs: https://playwright.dev/docs/api/class-testinfo
Hint: In VSC during debugging you can quickly find how this object looks like
Hint2: Copy value of object testInfo from debugger and paste in new js file. Then find best parameter what suite you best (example below in Notepad++)
you can put custom project configuration -
define your custom type TestOptions:
//testOptions.ts
import { test as base } from '#playwright/test';
export type TestOptions = {
person: string;
pageName: string;
weight: number;
};
export const test = base.extend<TestOptions>({
// change later in the playwright.config.ts
person: ['AAA', { option: true }],
pageName: ['nothing', { option: true }],
weight: [0, { option: true }],
});
in playwright.config.ts add actual custom properties for the project:
import { TestOptions } from './runner/testOptions';
...
projects: [
{
name: 'demo1',
use: { person: 'Alice', pageName: 'open1', weight: 150 },
},
{
name: 'demo2',
use: { person: 'Alex', pageName: 'open2', weight: 180 },
},
...
access specific properties as
test('test project properties', async ({ page }, testInfo) => {
console.log(`--- calling project: ${testInfo.project.name}`)
console.log(`--- project.person: ${testInfo.project.use["person"]}`)
console.log(`--- project.pageName: ${testInfo.project.use["pageName"]}`)
console.log(`--- project.weight: ${testInfo.project.use["weight"]}`)
});
call actual test for given project demo1
npx playwright test -g "test project properties" --project=demo1
output:
--- calling project: demo1
--- project.person: Alice
--- project.pageName: open1
--- project.weight: 150
Related
I'm developing a little CDKv2 script to instantiate a few AWS services.
I have some lambda code deployed in the lambda/ folder and the frontend stored in a bucket populated using the frontend/ folder in the source.
I've noticed that whenever I make a change to any of the file inside these two, cdk watch return the following error and falls back to perform a full redeploy (which is significantly slow).
Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: Parameter or resource 'DomainPrefix' could not be found for evaluation
Falling back to doing a full deployment
Is there any way to make changes in these folders only trigger updating the related bucket content or the related lambda?
Following here the stack.ts for quick reference, just in case here you can take a look at the repo.
export class CdkAuthWebappStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const domainPrefixParam = new CfnParameter(this, 'DomainPrefix', {
type: 'String',
description: 'You have to set it in google cloud as well', //(TODO: add link to explain properly)
default: process.env.DOMAIN_NAME || ''
})
const googleClientIdParam = new CfnParameter(this, 'GoogleClientId', {
type: 'String',
description: 'From google project',
noEcho: true,
default: process.env.GOOGLE_CLIENT_ID || ''
})
const googleClientSecretParam = new CfnParameter(this, 'GoogleClientSecret', {
type: 'String',
description: 'From google project',
noEcho: true,
default: process.env.GOOGLE_CLIENT_SECRET || ''
})
if(!domainPrefixParam.value || !googleClientIdParam.value || !googleClientSecretParam.value){
throw new Error('Make sure you initialized DomainPrefix, GoogleClientId and GoogleClientSecret in the stack parameters')
}
const s3frontend = new s3.Bucket(this, 'Bucket', {
bucketName: domainPrefixParam.valueAsString+'-frontend-bucket',
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
versioned: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
websiteIndexDocument: "index.html",
});
//TODO: fare in modo che questa origin access identity non sia legacy quando deployo
const cfdistributionoriginaccessidentity = new cloudfront.OriginAccessIdentity(this, 'CFOriginAccessIdentity', {
comment: "Used to give bucket read to cloudfront"
})
const cfdistribution = new cloudfront.CloudFrontWebDistribution(this, 'CFDistributionFrontend', {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: s3frontend,
originAccessIdentity: cfdistributionoriginaccessidentity
},
behaviors: [{
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
forwardedValues: {
queryString: true,
cookies: { forward: 'all' }
},
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.seconds(3600),
maxTtl: cdk.Duration.seconds(86400)
}]
}
]
})
s3frontend.grantRead(cfdistributionoriginaccessidentity)
const cfdistributionpolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['cloudfront:CreateInvalidation'],
resources: [`"arn:aws:cloudfront::${this.account}:distribution/${cfdistribution.distributionId}"`]
});
const userpool = new cognito.UserPool(this, 'WebAppUserPool', {
userPoolName: 'web-app-user-pool',
selfSignUpEnabled: false
})
const userpoolidentityprovidergoogle = new cognito.UserPoolIdentityProviderGoogle(this, 'WebAppUserPoolIdentityGoogle', {
clientId: googleClientIdParam.valueAsString,
clientSecret: googleClientSecretParam.valueAsString,
userPool: userpool,
attributeMapping: {
email: cognito.ProviderAttribute.GOOGLE_EMAIL
},
scopes: [ 'email' ]
})
// this is used to make the hostedui reachable
userpool.addDomain('Domain', {
cognitoDomain: {
domainPrefix: domainPrefixParam.valueAsString
}
})
const CLOUDFRONT_PUBLIC_URL = `https://${cfdistribution.distributionDomainName}/`
const client = userpool.addClient('Client', {
oAuth: {
flows: {
authorizationCodeGrant: true
},
callbackUrls: [
CLOUDFRONT_PUBLIC_URL
],
logoutUrls: [
CLOUDFRONT_PUBLIC_URL
],
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PHONE
]
},
supportedIdentityProviders: [
cognito.UserPoolClientIdentityProvider.GOOGLE
]
})
client.node.addDependency(userpoolidentityprovidergoogle)
// defines an AWS Lambda resource
const securedlambda = new lambda.Function(this, 'AuhtorizedRequestsHandler', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'secured.handler'
});
const lambdaapiintegration = new apigw.LambdaIntegration(securedlambda)
const backendapigw = new apigw.RestApi(this, 'AuthorizedRequestAPI', {
restApiName: domainPrefixParam.valueAsString,
defaultCorsPreflightOptions: {
"allowOrigins": apigw.Cors.ALL_ORIGINS,
"allowMethods": apigw.Cors.ALL_METHODS,
}
})
const backendapiauthorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'BackendAPIAuthorizer', {
cognitoUserPools: [userpool]
})
const authorizedresource = backendapigw.root.addMethod('GET', lambdaapiintegration, {
authorizer: backendapiauthorizer,
authorizationType: apigw.AuthorizationType.COGNITO
})
const s3deploymentfrontend = new s3deployment.BucketDeployment(this, 'DeployFrontEnd', {
sources: [
s3deployment.Source.asset('./frontend'),
s3deployment.Source.data('constants.js', `const constants = {domainPrefix:'${domainPrefixParam.valueAsString}', region:'${this.region}', cognito_client_id:'${client.userPoolClientId}', apigw_id:'${backendapigw.restApiId}'}`)
],
destinationBucket: s3frontend,
distribution: cfdistribution
})
new cdk.CfnOutput(this, 'YourPublicCloudFrontURL', {
value: CLOUDFRONT_PUBLIC_URL,
description: 'Navigate to the URL to access your deployed application'
})
}
}
Recording the solution from the comments:
Cause:
cdk watch apparently does not work with template parameters. I guess this is because the default --hotswap option bypasses CloudFormation and deploys instead via SDK commands.
Solution:
Remove the CfnParamters from the template. CDK recommends not using parameters in any case.
Perhaps cdk watch --no-hotswap would also work?
I want to compose fixtures. The first fixture should always be available (think of it as base class). second fixture will vary in different test files (think of it as derived class)
I tried following code and it's working as I was expecting. Is this ok to follow this approach or any better option available?
//baseFixture.js
import { test as base} from '#playwright/test';
interface MyFixtures {
fixture1: string;
}
export const test = base.extend<MyFixtures>({
fixture1: "fixture-one"
}, );
//derivedFixture.js
import {test as test1} from 'baseFixture'
interface MyFixtures2 {
fixture2: string;
}
export const test = test1.extend<MyFixtures2>({
fixture2: "fixture-two"
}, );
//in test_file.js
import {test} from 'derivedFixture'
test('should allow me use composed fixture', async ({ page, fixture1, fixture2 }) => {
console.log(`from first fixture ${fixture1}`)
console.log(`from second fixture ${fixture2}`)
});
Seems to me that you are using fixtures like POMs and you are overengineering tests. If it works for you and depending on what you want, then use it. If my assumption is correct instead of passing fixtures to another fixture pass POMs and you can even perform steps here to get that page into certain state and here is example from playwright page:
// my-test.js
const base = require('#playwright/test');
const { TodoPage } = require('./todo-page');
const { SettingsPage } = require('./settings-page');
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
exports.test = base.test.extend({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
exports.expect = base.expect;
Then in your test simply pass {todoPage} or {settingsPage} or both:
const { test, expect } = require('./my-test');
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.locator('.todo-item')).toContainText(['something nice']);
});
Also you can chain your fixtures here and reuse them, for eg. you could pass todoPage to settingsPage fixture:
settingsPage: async ({ todoPage}, use) => {
await use(new SettingsPage(page));
},
This way everything in todoPage will be executed then settingsPage and this is what you pass to your test, and I guess this is what you were trying to achive.
My approach is to use the base fixture as a dependent fixture in a derivative fixture:
import { test as base } from "#playwright/test"
interface MyFixtures1 {
fixture1: string
}
export const testBase = base.extend<{}, MyFixtures1>({
fixture1: [
async ({}, use) => {
console.log("fixture1 setup once per worker")
use("one")
console.log("fixture1 teardown once per worker")
},
{ scope: "worker" }
]
})
interface MyFixtures2 {
fixture2: string
}
export const test = testBase.extend<MyFixtures2>({
fixture2: async ({ fixture1 }, use) => {
console.log("fixture2 setup for each test")
use(`two-${fixture1}`)
console.log("fixture2 teardown for each test")
},
})
test("should allow me use composed fixture", async ({ fixture1, fixture2 }) => {
console.log(`from first fixture ${fixture1}`)
console.log(`from second fixture ${fixture2}`)
})
test("should base", async ({ fixture1 }) => {
console.log(`from first fixture ${fixture1}`)
})
test("should derived", async ({ fixture2 }) => {
console.log(`from second fixture ${fixture2}`)
})
More info about how to use fixtures in docs
In my Playwright tests, I set the base-url according to the docs:
const config: PlaywrightTestConfig = {
projects: [
{
name: 'Safari MacBook Air',
use: {
browserName: 'webkit',
viewport: {
width: 2560,
height: 1620,
},
contextOptions: {
ignoreHTTPSErrors: true,
},
},
},
],
use: {
baseURL: process.env.PLATFORMSH_URL,
headless: false,
locale: 'ja-JP',
// Debugging artifacts.
screenshot: 'on',
trace: 'on',
video: 'on',
},
};
export default config;
This is working for goto:
await this.page.goto('/myDirectory');
However, it fails for expect:
expect(page.url()).toBe('/myPage');
The error is:
Expected: "/myPage"
Received: "https://www.example.com/myPage"
How can I use expect with baseURL?
Try using this assertion instead:
For example, having configured Playwright homepage as our baseUrl
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
baseURL: 'https://playwright.dev/'
},
},
Then:
test('baseUrl', async ({ page }) => {
await page.goto('/docs/intro');
await expect(page).toHaveURL('/docs/intro');
});
If you'd like to continue using this format:
expect(page.url()).toBe('/myPage');
Then you need to change the assertion, because the url you're at is not equal to your directory. You can assert the url you're at contains the aforementioned directory, instead:
expect(page.url()).toContain('/myPage');
There is nothing wrong in your code except the last line.
page.url() will give you the whole URL address on wich your
driver (browser,whathever) currently is, and expect(something)toBe(thing) is like equals, and it will fail in your case.
You can assert in several ways like:
await expect(page.url().includes(partOfUrl)).toBeTruthy();
In the end I just wrapped it in my own utility function:
const pwaBaseUrl = process.env.NOSLASH_URL;
export const assertPath = async (page: Page, path: string) => {
expect(page.url()).toBe(`${pwaBaseUrl}/${path}`);
};
This guarantees that I am on the exact path. Perhaps this isn't generally necessary, but coming from Behat testing on PHP, this is what I'm used to.
How i can edit asset name? its doesnt work. Thanks
let assetService = $injector.get(self.ctx.servicesMap.get('assetService'));
let activeID = self.ctx.data[0].datasource.entityId
let tenantId = self.ctx.dashboard.authUser.tenantId
let asset = {
additionalInfo: null,
createdTime: 1599121131415, // временно
customerId: {
entityType: "CUSTOMER",
id: self.ctx.dashboard.authUser.customerId
},
id: {
entityType: "ASSET",
id: activeID
},
label: null,
name: "kuku", // временно
tenantId: {
entityType: "TENANT",
id: tenantId
},
type: "справочник"
}
assetService.saveAsset(asset)
Thingsboard is built using Angular 10 currently See releases. You correctly injected the Angular service 'assetService'. You need to follow the Angular method of subscribing to the observable from assetService.
Calling
assetService.saveAsset(asset)
without subscribing means nothing happens. From the Angular University Blog
The multiple versions of the Angular HTTP module all have an RxJS Observable-based API. This means that the multiple calls to the HTTP module will all return an observable, that we need to subscribe to one way or the other.
So here's the code to 'subscribe' to the observable described above
assetService.saveAsset(asset).subscribe(
(response) => {
console.log(
"saveAsset call Success:",
response);
},
response => {
console.log(
"saveAsset call Error:",
response);
},
() => {
console.log(
"saveAsset observable Complete"
);
});
Let me know if there's a mistake in the code above, I didn't test it. And thanks for your question Anzor - it led me to a solution to make a custom Thingsboard widget along with the Widgets Development Guide.
I am a 100% newb to Sencha and am trying to take a stab at re-factoring my company's mobile app.
Here is my app.js:
Ext.application({
name: 'RecruitTalkTouch',
views: ['Login'],
launch: function () {
Ext.Viewport.add([
{ xtype: 'loginview' }
]);
}
});
Login.js View:
Ext.define('RecruitTalkTouch.view.Login', {
extend: 'Ext.Container',
alias: "widget.loginview",
xtype: 'loginForm',
id: 'loginForm',
requires: ['Ext.form.FieldSet', 'Ext.form.Password', 'Ext.Label', 'Ext.Button' ],
config: {
title: 'Login',
items: [
{
xtype: 'label',
html: 'Login failed. Please enter the correct credentials.',
itemId: 'signInFailedLabel',
hidden: true,
hideAnimation: 'fadeOut',
showAnimation: 'fadeIn',
style: 'color:#990000;margin:5px 0px;'
},
{
xtype: 'fieldset',
title: 'Login Example',
items: [
{
xtype: 'textfield',
placeHolder: 'Email',
itemId: 'userNameTextField',
name: 'userNameTextField',
required: true
},
{
xtype: 'passwordfield',
placeHolder: 'Password',
itemId: 'passwordTextField',
name: 'passwordTextField',
required: true
}
]
},
{
xtype: 'button',
itemId: 'logInButton',
ui: 'action',
padding: '10px',
text: 'Log In'
}
]
}
});
Login.js Controller:
Ext.define('RecruitTalkTouch.controller.Login', {
extend: 'Ext.app.Controller',
config: {
refs: {
loginForm: 'loginForm'
},
control: {
'#logInButton': {
tap: 'onSignInCommand'
}
}
},
onSignInCommand: function(){
console.log("HELLO WORLD");
}
});
When I click the submit button, nothing happens. How can I hook up my submit button to listen for events (click, tap, etc) along with submitting the information to a backend API?
In app.js file of your application, add:
controllers: [
'Login'
]
in your application class. And for submitting information, call a Ajax request like this:
Ext.Ajax.request({
url: // api url..,
method: 'POST',
params: {
username: // get user name from form and put here,
password: // get password and ...
},
success: function(response) {
do something...
},
failure: function(err) {do ..}
});
from inside onSignInCommand() function.
You must activate your controller by adding it to the controllers option of your application class.
Then, to submit your data to the backend, you've got several options. You can use a form panel instead of your raw container, and use its submit method. Alternatively, you can use the Ext.Ajax singleton. In this case, you'll have to build the payload yourself. Finally, you can create a model with a configured proxy, and use its save method. This last way is probably the best for long term maintainability... Even if in the case of a simple login form, that may be a little bit overkill.
Can u please refer this sample app to create login form. Its very simple app please go through it.
http://miamicoder.com/2012/adding-a-login-screen-to-a-sencha-touch-application/